/* * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package io.reactivex.netty.protocol.http.internal; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.reactivex.netty.channel.Connection; import io.reactivex.netty.channel.ConnectionInputSubscriberEvent; import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridge.ConnectionInputSubscriber; import io.reactivex.netty.protocol.http.internal.AbstractHttpConnectionBridgeTest.AbstractHttpConnectionBridgeMock.HttpObject; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.mockito.Matchers; import org.mockito.Mockito; import rx.Producer; import rx.Subscriber; import rx.functions.Action0; import rx.observers.TestSubscriber; import rx.subscriptions.Subscriptions; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.List; import static io.netty.handler.codec.http.HttpUtil.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; public class AbstractHttpConnectionBridgeTest { @Rule public final HandlerRule handlerRule = new HandlerRule(); @Test(timeout = 60000) public void testSetTransferEncoding() throws Exception { DefaultHttpRequest reqWithNoContentLength = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); handlerRule.channel.writeAndFlush(reqWithNoContentLength); assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(1)); assertThat("Transfer encoding not set to chunked.", isTransferEncodingChunked(reqWithNoContentLength), is(true)); DefaultHttpRequest reqWithContentLength = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); setContentLength(reqWithContentLength, 100); handlerRule.channel.writeAndFlush(reqWithContentLength); /*One header from previous write*/ assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(2)); assertThat("Transfer encoding set to chunked when content length was set.", isTransferEncodingChunked(reqWithContentLength), is(false)); } @Test(timeout = 60000) public void testWritePrimitives() throws Exception { handlerRule.channel.writeAndFlush("Hello"); assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(1)); assertThat("Unexpected message written.", handlerRule.channel.readOutbound(), instanceOf(ByteBuf.class)); handlerRule.channel.writeAndFlush("Hello".getBytes()); assertThat("Unexpected outbound message count.", handlerRule.channel.outboundMessages(), hasSize(1)); assertThat("Unexpected message written.", handlerRule.channel.readOutbound(), instanceOf(ByteBuf.class)); } @Test(timeout = 60000) public void testConnInputSubscriberEvent() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); } @Test(timeout = 60000) public void testHttpContentSubscriberEventWithNoContentInputSub() throws Exception { TestSubscriber<String> subscriber = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(subscriber)); subscriber.assertTerminalEvent(); assertThat("Subscriber did not get an error", subscriber.getOnErrorEvents(), hasSize(1)); assertThat("Subscriber got an unexpected error", subscriber.getOnErrorEvents().get(0), instanceOf(NullPointerException.class)); } @Test(timeout = 60000) public void testHttpContentSub() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); handlerRule.simulateHeaderReceive(); /*Simulate header receive, required for content sub.*/ ProducerAwareSubscriber<String> subscriber = new ProducerAwareSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(subscriber)); subscriber.assertNoErrors(); @SuppressWarnings("unchecked") Subscriber<String> contentSub = (Subscriber<String>) handlerRule.connInSub.getState().getContentSub(); assertThat("Unexpected HTTP Content subscriber found", contentSub, equalTo((Subscriber<String>)subscriber)); assertThat("Unexpected content subscriber producer.", subscriber.getProducer(), equalTo(handlerRule.connInputProducerMock)); subscriber.unsubscribe(); subscriber.assertUnsubscribed(); assertThat("Unsubscribing from HTTP content, did not unsubscribe from connection input.", handlerRule.connInSub.isUnsubscribed(), is(true)); } @Test(timeout = 60000) public void testContentArrivedBeforeSubscription() throws Exception { handlerRule.channel.config().setAutoRead(false); handlerRule.setupAndAssertConnectionInputSub(); handlerRule.connInSub.onNext(new DefaultLastHttpContent());/*Simulating content read on channel*/ TestSubscriber<String> contentSub = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); contentSub.assertTerminalEvent(); assertThat("Content received on delayed subscription.", contentSub.getOnNextEvents(), is(empty())); assertThat("Error not received on delayed subscription.", contentSub.getOnErrorEvents(), hasSize(1)); } @Test(timeout = 60000) public void testLazyContentAndTrailerSubWithAutoReadOn() throws Exception { handlerRule.channel.config().setAutoRead(true); handlerRule.setupAndAssertConnectionInputSub(); /*Request sent, no content/trailer sub registered, will cause error on sub.*/ handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); TestSubscriber<String> contentSub = new TestSubscriber<>(); /*Lazy subscription*/ handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); contentSub.assertTerminalEvent(); assertThat("Content received on lazy subscription.", contentSub.getOnNextEvents(), is(empty())); assertThat("Error not received on lazy subscription.", contentSub.getOnErrorEvents(), hasSize(1)); } @Test(timeout = 60000) public void testLazyContentAndTrailerSubWithAutoReadOff() throws Exception { handlerRule.channel.config().setAutoRead(false); handlerRule.setupAndAssertConnectionInputSub(); /*Request sent, after this it will expect the subscriber to be registered before content arrives..*/ handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); /*Content sent but no subscriber.*/ handlerRule.connInSub.onNext(new DefaultHttpContent(Unpooled.buffer().writeBytes("Hello".getBytes()))); TestSubscriber<String> contentSub = new TestSubscriber<>(); /*Content already sent, lazy sub now.*/ handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); contentSub.assertTerminalEvent(); assertThat("Content received on lazy subscription.", contentSub.getOnNextEvents(), is(empty())); assertThat("Error not received on lazy subscription.", contentSub.getOnErrorEvents(), hasSize(1)); handlerRule.connInSub.onNext(new DefaultLastHttpContent());/*Simulate completion.*/ } @Test(timeout = 60000) public void testHttpChunked() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); handlerRule.simulateHeaderReceive(); /*Eager content subscription*/ TestSubscriber<ByteBuf> contentSub = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); /*Headers sent*/ handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); ByteBuf content1 = Unpooled.buffer().writeBytes("Hello".getBytes()); ByteBuf content2 = Unpooled.buffer().writeBytes("Hello2".getBytes()); ByteBuf contentLast = Unpooled.buffer().writeBytes("Hello3".getBytes()); /*Content 1 sent.*/ handlerRule.connInSub.onNext(new DefaultHttpContent(content1)); /*Content 2 sent.*/ handlerRule.connInSub.onNext(new DefaultHttpContent(content2)); DefaultLastHttpContent trailers = new DefaultLastHttpContent(contentLast); String trailer1Name = "foo"; String trailer1Value = "bar"; trailers.trailingHeaders().add(trailer1Name, trailer1Value); /*trailers with content*/ handlerRule.connInSub.onNext(trailers); contentSub.assertTerminalEvent(); contentSub.assertNoErrors(); assertThat("Unexpected content chunks.", contentSub.getOnNextEvents(), hasSize(3)); assertThat("Unexpected content chunks.", contentSub.getOnNextEvents(), contains(content1, content2, contentLast)); } @Test(timeout = 60000) public void testClose() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); /*Eager content subscription*/ TestSubscriber<ByteBuf> contentSub = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); /*Headers sent*/ handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); /*Close before response complete*/ handlerRule.channel.close(); handlerRule.headerSub.assertTerminalEvent(); contentSub.assertTerminalEvent(); assertThat("No error to header subscriber on close.", handlerRule.headerSub.getOnErrorEvents(), hasSize(1)); assertThat("No error to content subscriber on close.", contentSub.getOnErrorEvents(), hasSize(1)); assertThat("Close before complete did not get invoked.", ((AbstractHttpConnectionBridgeMock)handlerRule.handler).closedBeforeReceive, is(true)); } @Test(timeout = 60000) public void testHeaderUnsubscribeBeforeHeaderReceive() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); handlerRule.headerSub.unsubscribe(); assertThat("Connection input not unsubscribed.", handlerRule.connInSub.isUnsubscribed(), is(true)); } @Test(timeout = 60000) public void testHeaderUnsubscribeAfterHeaderReceive() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); /*Headers sent*/ handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); handlerRule.headerSub.unsubscribe(); assertThat("Connection input unsubscribed post headers.", handlerRule.connInSub.isUnsubscribed(), is(false)); } @Test(timeout = 60000) public void testConnectionInputCompleteWithNoHeaders() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); handlerRule.simulateHeaderReceive(); /*Eager content subscription*/ TestSubscriber<ByteBuf> contentSub = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); handlerRule.connInSub.onCompleted(); handlerRule.headerSub.assertTerminalEvent(); /*Since headers started but not content*/ handlerRule.headerSub.assertError(ClosedChannelException.class); contentSub.assertTerminalEvent(); contentSub.assertError(ClosedChannelException.class); } @Test(timeout = 60000) public void testConnectionInputCompletePostHeaders() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); /*Eager content subscription*/ TestSubscriber<ByteBuf> contentSub = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); /*Headers sent*/ handlerRule.connInSub.onNext(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")); handlerRule.headerSub.assertNoErrors(); assertThat("Header subscriber did not get the headers.", handlerRule.headerSub.getOnNextEvents(), hasSize(1)); /*Look only for one HTTP message*/ handlerRule.headerSub.unsubscribe(); assertThat("Content subscriber unsubscribed post header unsubscribe.", contentSub.isUnsubscribed(), is(false)); handlerRule.connInSub.onCompleted(); contentSub.assertTerminalEvent(); assertThat("Content subscriber did not get an error.", contentSub.getOnErrorEvents(), hasSize(1)); } @Test(timeout = 60000) public void testMultiSubscribers() throws Exception { handlerRule.setupAndAssertConnectionInputSub(); handlerRule.simulateHeaderReceive(); /*Eager content subscription*/ TestSubscriber<ByteBuf> contentSub = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub)); @SuppressWarnings("unchecked") Subscriber<ByteBuf> contentSubFound = (Subscriber<ByteBuf>) handlerRule.connInSub.getState().getContentSub(); assertThat("Unexpected HTTP Content subscriber found", contentSubFound, equalTo((Subscriber<ByteBuf>) contentSub)); contentSub.assertNoErrors(); /*Second active subscription*/ TestSubscriber<ByteBuf> contentSub2 = new TestSubscriber<>(); handlerRule.channel.pipeline().fireUserEventTriggered(new HttpContentSubscriberEvent<>(contentSub2)); contentSub2.assertTerminalEvent(); assertThat("Second content subscriber did not get an error.", contentSub2.getOnErrorEvents(), hasSize(1)); } public static class HandlerRule extends ExternalResource { private Connection<String, String> connMock; private EmbeddedChannel channel; private AbstractHttpConnectionBridge<String> handler; private EventCatcher eventCatcher; private ConnectionInputSubscriber connInSub; private Producer connInputProducerMock; private ProducerAwareSubscriber<HttpObject> headerSub; @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { handler = newAbstractHttpConnectionBridgeMock(); eventCatcher = new EventCatcher(); channel = new EmbeddedChannel(handler, eventCatcher); @SuppressWarnings("unchecked") Connection<String, String> connMock = Mockito.mock(Connection.class); Mockito.when(connMock.unsafeNettyChannel()).thenReturn(channel); HandlerRule.this.connMock = connMock; base.evaluate(); } }; } protected AbstractHttpConnectionBridge<String> newAbstractHttpConnectionBridgeMock() { return new AbstractHttpConnectionBridgeMock(HttpRequest.class); } public EmbeddedChannel getChannel() { return channel; } public void simulateHeaderReceive() { connInSub.getState().headerReceived(); } public void setupAndAssertConnectionInputSub() { headerSub = new ProducerAwareSubscriber<>(); @SuppressWarnings({"rawtypes", "unchecked"}) ConnectionInputSubscriberEvent evt = new ConnectionInputSubscriberEvent(headerSub); channel.pipeline().fireUserEventTriggered(evt); assertThat("Handler did not pass the event.", eventCatcher.events, hasSize(1)); assertThat("Handler did not modify the event.", eventCatcher.events, not(contains((Object) evt))); Object eventCaught = eventCatcher.events.get(0); assertThat("Unexpected propagated event.", eventCaught, instanceOf(ConnectionInputSubscriberEvent.class)); @SuppressWarnings({"rawtypes", "unchecked"}) ConnectionInputSubscriberEvent modEvt = (ConnectionInputSubscriberEvent) eventCaught; assertThat("Unexpected propagated event subscriber.", modEvt.getSubscriber(), instanceOf(ConnectionInputSubscriber.class)); @SuppressWarnings("unchecked") ConnectionInputSubscriber connInSub = (ConnectionInputSubscriber) modEvt.getSubscriber(); this.connInSub = connInSub; assertThat("Channel not set in the subscriber.", connInSub.getChannel(), is(notNullValue())); assertThat("Unexpected channel set in the subscriber.", connInSub.getChannel(), equalTo((Channel)channel)); @SuppressWarnings("unchecked") Subscriber<HttpObject> headerSub = (Subscriber<HttpObject>) connInSub.getState().getHeaderSub(); assertThat("Unexpected header subscriber.", headerSub, is((Subscriber<HttpObject>) this.headerSub)); connInputProducerMock = Mockito.mock(Producer.class); connInSub.setProducer(connInputProducerMock); assertThat("Header subscriber producer not set.", this.headerSub.getProducer(), equalTo(connInputProducerMock)); Mockito.verify(connInputProducerMock).request(Matchers.anyLong()); } } private static class ProducerAwareSubscriber<T> extends TestSubscriber<T> { private Producer producer; @Override public void setProducer(Producer producer) { this.producer = producer; super.setProducer(producer); } public Producer getProducer() { return producer; } } private static class EventCatcher extends ChannelDuplexHandler { private final List<Object> events = new ArrayList<>(); @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { events.add(evt); super.userEventTriggered(ctx, evt); } } public static class AbstractHttpConnectionBridgeMock extends AbstractHttpConnectionBridge<String> { private final Class<?> headerMsgClass; private volatile boolean closedBeforeReceive; public AbstractHttpConnectionBridgeMock(Class<?> headerMsgClass) { this.headerMsgClass = headerMsgClass; } @Override protected boolean isInboundHeader(Object nextItem) { return headerMsgClass.isAssignableFrom(nextItem.getClass()); } @Override protected boolean isOutboundHeader(Object nextItem) { return nextItem instanceof HttpRequest; } @Override protected Object newHttpObject(Object nextItem, Channel channel) { return new HttpObject(); } @Override protected void onContentReceived() { // No Op } @Override protected void onContentReceiveComplete(long receiveStartTimeNanos) { // No Op } @Override protected void beforeOutboundHeaderWrite(HttpMessage httpMsg, ChannelPromise promise, long startTimeNanos) { // No Op } @Override protected void onOutboundLastContentWrite(LastHttpContent msg, ChannelPromise promise, long headerWriteStartTimeNanos) { // No Op } @Override protected void onClosedBeforeReceiveComplete(Channel channel) { closedBeforeReceive = true; } @Override protected void onNewContentSubscriber(final ConnectionInputSubscriber inputSubscriber, Subscriber<? super String> newSub) { newSub.add(Subscriptions.create(new Action0() { @Override public void call() { inputSubscriber.unsubscribe(); } })); } public static class HttpObject { } } }