();
+
+ public final AtomicBoolean closing = new AtomicBoolean(false);
+ }
+
+ private static final class QueuedResponse
+ {
+ public ChannelBuffer data;
+
+ public ChannelFuture writeFuture;
+
+ QueuedResponse(ChannelBuffer data, ChannelFuture writeFuture)
+ {
+ this.data = data;
+ this.writeFuture = writeFuture;
+ }
+ }
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchDownstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchDownstreamInterface.java
new file mode 100644
index 0000000000..6e4e8552c1
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchDownstreamInterface.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+package org.jboss.netty.channel.socket.httptunnel;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelFuture;
+
+/**
+ * The interface from a HttpTunnelAcceptedChannel to the ServerMessageSwitch.
+ * This primarily exists for mock object testing purposes.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+interface ServerMessageSwitchDownstreamInterface
+{
+
+ public void serverCloseTunnel(String tunnelId);
+
+ public void routeOutboundData(String tunnelId, ChannelBuffer data, ChannelFuture writeFuture);
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchUpstreamInterface.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchUpstreamInterface.java
new file mode 100644
index 0000000000..9491f6bd46
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchUpstreamInterface.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.Channel;
+
+/**
+ * The interface from a TCP channel which is being used to communicate with the client
+ * end of an http tunnel and the server message switch.
+ *
+ * This primarily exists for mock testing purposes.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+interface ServerMessageSwitchUpstreamInterface
+{
+
+ public String createTunnel(InetSocketAddress remoteAddress);
+
+ public boolean isOpenTunnel(String tunnelId);
+
+ public void clientCloseTunnel(String tunnelId);
+
+ /**
+ * Passes some received data from a client for forwarding to the server's view
+ * of the tunnel.
+ * @return the current status of the tunnel. ALIVE indicates the tunnel is still
+ * functional, CLOSED indicates it is closed and the client should be notified
+ * of this (and will be forgotten after this notification).
+ */
+ public TunnelStatus routeInboundData(String tunnelId, ChannelBuffer inboundData);
+
+ public void pollOutboundData(String tunnelId, Channel responseChannel);
+
+ public static enum TunnelStatus {
+ ALIVE, CLOSED
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelIdGenerator.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelIdGenerator.java
new file mode 100644
index 0000000000..75a5388917
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelIdGenerator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+/**
+ * This interface is used by the server end of an http tunnel to generate new
+ * tunnel ids for accepted client connections.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+public interface TunnelIdGenerator
+{
+
+ /**
+ * Generates the next tunnel ID to be used, which must be unique
+ * (i.e. ensure with high probability that it will not clash with
+ * an existing tunnel ID). This method must be thread safe, and
+ * preferably lock free.
+ */
+ public String generateId();
+
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelWrappedServerChannelHandler.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelWrappedServerChannelHandler.java
new file mode 100644
index 0000000000..6f3ba2796a
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/TunnelWrappedServerChannelHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.net.SocketAddress;
+
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.ChildChannelStateEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+class TunnelWrappedServerChannelHandler extends SimpleChannelUpstreamHandler
+{
+
+ public static final String NAME = "TunnelWrappedServerChannelHandler";
+
+ private final HttpTunnelServerChannel tunnelChannel;
+
+ private final AcceptedServerChannelPipelineFactory pipelineFactory;
+
+ private final ChannelGroup allChannels;
+
+ public TunnelWrappedServerChannelHandler(HttpTunnelServerChannel tunnelChannel,
+ AcceptedServerChannelPipelineFactory pipelineFactory, ChannelGroup allChannels)
+ {
+ this.tunnelChannel = tunnelChannel;
+ this.pipelineFactory = pipelineFactory;
+ this.allChannels = allChannels;
+ }
+
+ @Override
+ public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ e.getChannel().getConfig().setPipelineFactory(pipelineFactory);
+ super.channelOpen(ctx, e);
+ }
+
+ @Override
+ public void channelBound(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ Channels.fireChannelBound(tunnelChannel, (SocketAddress) e.getValue());
+ super.channelBound(ctx, e);
+ }
+
+ @Override
+ public void channelUnbound(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ Channels.fireChannelUnbound(tunnelChannel);
+ super.channelUnbound(ctx, e);
+ }
+
+ @Override
+ public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ Channels.fireChannelClosed(tunnelChannel);
+ super.channelClosed(ctx, e);
+ }
+
+ @Override
+ public void childChannelOpen(ChannelHandlerContext ctx, ChildChannelStateEvent e) throws Exception
+ {
+ allChannels.add(e.getChildChannel());
+ }
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenter.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenter.java
new file mode 100644
index 0000000000..5236f3497e
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelDownstreamHandler;
+
+/**
+ * Downstream handler which places an upper bound on the size of written
+ * {@link ChannelBuffer ChannelBuffers}. If a buffer
+ * is bigger than the specified upper bound, the buffer is broken up
+ * into two or more smaller pieces.
+ *
+ * This is utilised by the http tunnel to smooth out the per-byte latency,
+ * by placing an upper bound on HTTP request / response body sizes.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+public class WriteFragmenter extends SimpleChannelDownstreamHandler
+{
+
+ public static final String NAME = "writeFragmenter";
+
+ private int splitThreshold;
+
+ public WriteFragmenter(int splitThreshold)
+ {
+ this.splitThreshold = splitThreshold;
+ }
+
+ public void setSplitThreshold(int splitThreshold)
+ {
+ this.splitThreshold = splitThreshold;
+ }
+
+ @Override
+ public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception
+ {
+ ChannelBuffer data = (ChannelBuffer) e.getMessage();
+
+ if (data.readableBytes() <= splitThreshold)
+ {
+ super.writeRequested(ctx, e);
+ }
+ else
+ {
+ List fragments = WriteSplitter.split(data, splitThreshold);
+ ChannelFutureAggregator aggregator = new ChannelFutureAggregator(e.getFuture());
+ for (ChannelBuffer fragment : fragments)
+ {
+ ChannelFuture fragmentFuture = Channels.future(ctx.getChannel(), true);
+ aggregator.addFuture(fragmentFuture);
+ Channels.write(ctx, fragmentFuture, fragment);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitter.java b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitter.java
new file mode 100644
index 0000000000..e3cf664f9d
--- /dev/null
+++ b/src/main/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitter.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+
+/**
+ * Provides functionality to split a provided ChannelBuffer into multiple fragments which fit
+ * under a specified size threshold.
+ *
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ * @author OneDrum Ltd.
+ */
+public final class WriteSplitter
+{
+
+ public static List split(ChannelBuffer buffer, int splitThreshold)
+ {
+ int listSize = (int) ((float) buffer.readableBytes() / splitThreshold);
+ ArrayList fragmentList = new ArrayList(listSize);
+
+ if (buffer.readableBytes() > splitThreshold)
+ {
+ int slicePosition = buffer.readerIndex();
+ while (slicePosition < buffer.writerIndex())
+ {
+ int chunkSize = Math.min(splitThreshold, buffer.writerIndex() - slicePosition);
+ ChannelBuffer chunk = buffer.slice(slicePosition, chunkSize);
+ fragmentList.add(chunk);
+ slicePosition += chunkSize;
+ }
+ }
+ else
+ {
+ fragmentList.add(ChannelBuffers.wrappedBuffer(buffer));
+ }
+
+ return fragmentList;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatchTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatchTest.java
new file mode 100644
index 0000000000..b219566a49
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/AcceptedServerChannelRequestDispatchTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class AcceptedServerChannelRequestDispatchTest
+{
+
+ private static final String HOST = "test.server.com";
+
+ private static final String KNOWN_TUNNEL_ID = "1";
+
+ protected static final String UNKNOWN_TUNNEL_ID = "unknownTunnel";
+
+ JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private AcceptedServerChannelRequestDispatch handler;
+
+ FakeSocketChannel channel;
+
+ private FakeChannelSink sink;
+
+ ServerMessageSwitchUpstreamInterface messageSwitch;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ messageSwitch = mockContext.mock(ServerMessageSwitchUpstreamInterface.class);
+ handler = new AcceptedServerChannelRequestDispatch(messageSwitch);
+ pipeline.addLast(AcceptedServerChannelRequestDispatch.NAME, handler);
+ sink = new FakeChannelSink();
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ channel.remoteAddress = InetSocketAddress.createUnresolved("test.client.com", 51231);
+
+ mockContext.checking(new Expectations()
+ {
+ {
+ ignoring(messageSwitch).isOpenTunnel(KNOWN_TUNNEL_ID);
+ will(returnValue(true));
+ }
+ });
+ }
+
+ @Test
+ public void testTunnelOpenRequest()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).createTunnel(channel.remoteAddress);
+ will(returnValue(KNOWN_TUNNEL_ID));
+ }
+ });
+
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createOpenTunnelRequest(HOST));
+ assertEquals(1, sink.events.size());
+ HttpResponse response = NettyTestUtils.checkIsDownstreamMessageEvent(sink.events.poll(), HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isTunnelOpenResponse(response));
+ }
+
+ @Test
+ public void testTunnelCloseRequest()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).clientCloseTunnel(KNOWN_TUNNEL_ID);
+ }
+ });
+
+ HttpRequest request = HttpTunnelMessageUtils.createCloseTunnelRequest(HOST, KNOWN_TUNNEL_ID);
+ Channels.fireMessageReceived(channel, request);
+ assertEquals(1, sink.events.size());
+ ChannelEvent responseEvent = sink.events.poll();
+ HttpResponse response = NettyTestUtils.checkIsDownstreamMessageEvent(responseEvent, HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isTunnelCloseResponse(response));
+ checkClosesAfterWrite(responseEvent);
+ }
+
+ @Test
+ public void testTunnelCloseRequestWithoutTunnelIdRejected()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createCloseTunnelRequest(HOST, null);
+ checkRequestWithoutTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testTunnelCloseRequestWithUnknownTunnelId()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createCloseTunnelRequest(HOST, UNKNOWN_TUNNEL_ID);
+ checkRequestWithUnknownTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testSendDataRequest()
+ {
+ final ChannelBuffer expectedData = ChannelBuffers.dynamicBuffer();
+ expectedData.writeLong(1234L);
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).routeInboundData(KNOWN_TUNNEL_ID, expectedData);
+ }
+ });
+
+ HttpRequest request = HttpTunnelMessageUtils.createSendDataRequest(HOST, KNOWN_TUNNEL_ID, expectedData);
+ Channels.fireMessageReceived(channel, request);
+
+ assertEquals(1, sink.events.size());
+ HttpResponse response = NettyTestUtils.checkIsDownstreamMessageEvent(sink.events.poll(), HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isOKResponse(response));
+ }
+
+ @Test
+ public void testSendDataRequestWithNoContentRejected()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createSendDataRequest(HOST, KNOWN_TUNNEL_ID,
+ ChannelBuffers.dynamicBuffer());
+ Channels.fireMessageReceived(channel, request);
+
+ assertEquals(1, sink.events.size());
+ checkResponseIsRejection("Send data requests must contain data");
+ }
+
+ @Test
+ public void testSendDataRequestForUnknownTunnelIdRejected()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createSendDataRequest(HOST, UNKNOWN_TUNNEL_ID,
+ ChannelBuffers.dynamicBuffer());
+ checkRequestWithUnknownTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testSendDataRequestWithoutTunnelIdRejected()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createSendDataRequest(HOST, null, ChannelBuffers.dynamicBuffer());
+ checkRequestWithoutTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testReceiveDataRequest()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).pollOutboundData(KNOWN_TUNNEL_ID, channel);
+ }
+ });
+ HttpRequest request = HttpTunnelMessageUtils.createReceiveDataRequest(HOST, KNOWN_TUNNEL_ID);
+ Channels.fireMessageReceived(channel, request);
+ }
+
+ @Test
+ public void testReceiveDataRequestWithoutTunnelIdRejected()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createReceiveDataRequest(HOST, null);
+ checkRequestWithoutTunnelIdIsRejected(request);
+ }
+
+ @Test
+ public void testReceiveDataRequestForUnknownTunnelIdRejected()
+ {
+ HttpRequest request = HttpTunnelMessageUtils.createReceiveDataRequest(HOST, UNKNOWN_TUNNEL_ID);
+ checkRequestWithUnknownTunnelIdIsRejected(request);
+ }
+
+ private void checkRequestWithoutTunnelIdIsRejected(HttpRequest request)
+ {
+ Channels.fireMessageReceived(channel, request);
+ assertEquals(1, sink.events.size());
+ ChannelEvent responseEvent = checkResponseIsRejection("no tunnel id specified in request");
+ checkClosesAfterWrite(responseEvent);
+ }
+
+ private void checkRequestWithUnknownTunnelIdIsRejected(HttpRequest request)
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).isOpenTunnel(UNKNOWN_TUNNEL_ID);
+ will(returnValue(false));
+ }
+ });
+
+ Channels.fireMessageReceived(channel, request);
+ assertEquals(1, sink.events.size());
+ ChannelEvent responseEvent = checkResponseIsRejection("specified tunnel is either closed or does not exist");
+ checkClosesAfterWrite(responseEvent);
+ }
+
+ private ChannelEvent checkResponseIsRejection(String errorMessage)
+ {
+ ChannelEvent responseEvent = sink.events.poll();
+
+ HttpResponse response = NettyTestUtils.checkIsDownstreamMessageEvent(responseEvent, HttpResponse.class);
+ assertTrue(HttpTunnelMessageUtils.isRejection(response));
+ assertEquals(errorMessage, HttpTunnelMessageUtils.extractErrorMessage(response));
+
+ return responseEvent;
+ }
+
+ private void checkClosesAfterWrite(ChannelEvent responseEvent)
+ {
+ responseEvent.getFuture().setSuccess();
+ assertEquals(1, sink.events.size());
+ NettyTestUtils.checkIsStateEvent(sink.events.poll(), ChannelState.OPEN, false);
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelConfig.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelConfig.java
new file mode 100644
index 0000000000..1b853da685
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelConfig.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.jboss.netty.buffer.ChannelBufferFactory;
+import org.jboss.netty.buffer.HeapChannelBufferFactory;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.SocketChannelConfig;
+import org.jboss.netty.util.internal.ConversionUtil;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeChannelConfig implements SocketChannelConfig
+{
+
+ private int receiveBufferSize = 1024;
+
+ private int sendBufferSize = 1024;
+
+ private int soLinger = 500;
+
+ private int trafficClass = 0;
+
+ private boolean keepAlive = true;
+
+ private boolean reuseAddress = true;
+
+ private boolean tcpNoDelay = false;
+
+ private ChannelBufferFactory bufferFactory = new HeapChannelBufferFactory();
+
+ private int connectTimeout = 5000;
+
+ private ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory()
+ {
+ public ChannelPipeline getPipeline() throws Exception
+ {
+ return Channels.pipeline();
+ }
+ };
+
+ private int writeTimeout = 3000;
+
+ public int getReceiveBufferSize()
+ {
+ return receiveBufferSize;
+ }
+
+ public void setReceiveBufferSize(int receiveBufferSize)
+ {
+ this.receiveBufferSize = receiveBufferSize;
+ }
+
+ public int getSendBufferSize()
+ {
+ return sendBufferSize;
+ }
+
+ public void setSendBufferSize(int sendBufferSize)
+ {
+ this.sendBufferSize = sendBufferSize;
+ }
+
+ public int getSoLinger()
+ {
+ return soLinger;
+ }
+
+ public void setSoLinger(int soLinger)
+ {
+ this.soLinger = soLinger;
+ }
+
+ public int getTrafficClass()
+ {
+ return trafficClass;
+ }
+
+ public void setTrafficClass(int trafficClass)
+ {
+ this.trafficClass = trafficClass;
+ }
+
+ public boolean isKeepAlive()
+ {
+ return keepAlive;
+ }
+
+ public void setKeepAlive(boolean keepAlive)
+ {
+ this.keepAlive = keepAlive;
+ }
+
+ public boolean isReuseAddress()
+ {
+ return reuseAddress;
+ }
+
+ public void setReuseAddress(boolean reuseAddress)
+ {
+ this.reuseAddress = reuseAddress;
+ }
+
+ public boolean isTcpNoDelay()
+ {
+ return tcpNoDelay;
+ }
+
+ public void setTcpNoDelay(boolean tcpNoDelay)
+ {
+ this.tcpNoDelay = tcpNoDelay;
+ }
+
+ public void setPerformancePreferences(int connectionTime, int latency, int bandwidth)
+ {
+ // do nothing
+ }
+
+ public ChannelBufferFactory getBufferFactory()
+ {
+ return bufferFactory;
+ }
+
+ public void setBufferFactory(ChannelBufferFactory bufferFactory)
+ {
+ this.bufferFactory = bufferFactory;
+ }
+
+ public int getConnectTimeoutMillis()
+ {
+ return connectTimeout;
+ }
+
+ public void setConnectTimeoutMillis(int connectTimeoutMillis)
+ {
+ connectTimeout = connectTimeoutMillis;
+ }
+
+ public ChannelPipelineFactory getPipelineFactory()
+ {
+ return pipelineFactory;
+ }
+
+ public void setPipelineFactory(ChannelPipelineFactory pipelineFactory)
+ {
+ this.pipelineFactory = pipelineFactory;
+ }
+
+ public int getWriteTimeoutMillis()
+ {
+ return writeTimeout;
+ }
+
+ public void setWriteTimeoutMillis(int writeTimeoutMillis)
+ {
+ writeTimeout = writeTimeoutMillis;
+ }
+
+ public boolean setOption(String key, Object value)
+ {
+ if (key.equals("pipelineFactory"))
+ {
+ setPipelineFactory((ChannelPipelineFactory) value);
+ }
+ else if (key.equals("connectTimeoutMillis"))
+ {
+ setConnectTimeoutMillis(ConversionUtil.toInt(value));
+ }
+ else if (key.equals("bufferFactory"))
+ {
+ setBufferFactory((ChannelBufferFactory) value);
+ }
+ else if (key.equals("receiveBufferSize"))
+ {
+ setReceiveBufferSize(ConversionUtil.toInt(value));
+ }
+ else if (key.equals("sendBufferSize"))
+ {
+ setSendBufferSize(ConversionUtil.toInt(value));
+ }
+ else if (key.equals("tcpNoDelay"))
+ {
+ setTcpNoDelay(ConversionUtil.toBoolean(value));
+ }
+ else if (key.equals("keepAlive"))
+ {
+ setKeepAlive(ConversionUtil.toBoolean(value));
+ }
+ else if (key.equals("reuseAddress"))
+ {
+ setReuseAddress(ConversionUtil.toBoolean(value));
+ }
+ else if (key.equals("soLinger"))
+ {
+ setSoLinger(ConversionUtil.toInt(value));
+ }
+ else if (key.equals("trafficClass"))
+ {
+ setTrafficClass(ConversionUtil.toInt(value));
+ }
+ else
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public void setOptions(Map options)
+ {
+ for (Entry e : options.entrySet())
+ {
+ setOption(e.getKey(), e.getValue());
+ }
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelSink.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelSink.java
new file mode 100644
index 0000000000..7ed63f9ac8
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeChannelSink.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.jboss.netty.channel.AbstractChannelSink;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeChannelSink extends AbstractChannelSink
+{
+
+ public Queue events = new LinkedList();
+
+ public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) throws Exception
+ {
+ events.add(e);
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeClientSocketChannelFactory.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeClientSocketChannelFactory.java
new file mode 100644
index 0000000000..3958b01171
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeClientSocketChannelFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.SocketChannel;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeClientSocketChannelFactory implements ClientSocketChannelFactory
+{
+
+ public List createdChannels;
+
+ public FakeClientSocketChannelFactory()
+ {
+ createdChannels = new ArrayList();
+ }
+
+ public SocketChannel newChannel(ChannelPipeline pipeline)
+ {
+ FakeSocketChannel channel = new FakeSocketChannel(null, this, pipeline, new FakeChannelSink());
+ createdChannels.add(channel);
+ return channel;
+ }
+
+ public void releaseExternalResources()
+ {
+ // nothing to do
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannel.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannel.java
new file mode 100644
index 0000000000..0bda1e4f6a
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannel.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.jboss.netty.channel.Channels.fireChannelBound;
+import static org.jboss.netty.channel.Channels.fireChannelConnected;
+import static org.jboss.netty.channel.Channels.fireChannelOpen;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.channel.AbstractChannel;
+import org.jboss.netty.channel.ChannelFactory;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelSink;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jboss.netty.channel.socket.ServerSocketChannelConfig;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeServerSocketChannel extends AbstractChannel implements ServerSocketChannel
+{
+
+ public boolean bound;
+
+ public boolean connected;
+
+ public InetSocketAddress remoteAddress;
+
+ public InetSocketAddress localAddress;
+
+ public ServerSocketChannelConfig config = new FakeServerSocketChannelConfig();
+
+ public FakeServerSocketChannel(ChannelFactory factory, ChannelPipeline pipeline, ChannelSink sink)
+ {
+ super(null, factory, pipeline, sink);
+ }
+
+ public ServerSocketChannelConfig getConfig()
+ {
+ return config;
+ }
+
+ public InetSocketAddress getLocalAddress()
+ {
+ return localAddress;
+ }
+
+ public InetSocketAddress getRemoteAddress()
+ {
+ return remoteAddress;
+ }
+
+ public boolean isBound()
+ {
+ return bound;
+ }
+
+ public boolean isConnected()
+ {
+ return connected;
+ }
+
+ public FakeSocketChannel acceptNewConnection(InetSocketAddress remoteAddress, ChannelSink sink) throws Exception
+ {
+ ChannelPipeline newPipeline = getConfig().getPipelineFactory().getPipeline();
+ FakeSocketChannel newChannel = new FakeSocketChannel(this, getFactory(), newPipeline, sink);
+ newChannel.localAddress = localAddress;
+ newChannel.remoteAddress = remoteAddress;
+ fireChannelOpen(newChannel);
+ fireChannelBound(newChannel, newChannel.localAddress);
+ fireChannelConnected(this, newChannel.remoteAddress);
+
+ return newChannel;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelConfig.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelConfig.java
new file mode 100644
index 0000000000..539456c343
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelConfig.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import org.jboss.netty.buffer.ChannelBufferFactory;
+import org.jboss.netty.buffer.HeapChannelBufferFactory;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.DefaultChannelConfig;
+import org.jboss.netty.channel.socket.ServerSocketChannelConfig;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeServerSocketChannelConfig extends DefaultChannelConfig implements ServerSocketChannelConfig
+{
+
+ public int backlog = 5;
+
+ public int receiveBufferSize = 1024;
+
+ public boolean reuseAddress = false;
+
+ public int connectionTimeout = 5000;
+
+ public ChannelPipelineFactory pipelineFactory;
+
+ public int writeTimeout = 5000;
+
+ public ChannelBufferFactory bufferFactory = new HeapChannelBufferFactory();
+
+ public int getBacklog()
+ {
+ return backlog;
+ }
+
+ public void setBacklog(int backlog)
+ {
+ this.backlog = backlog;
+ }
+
+ public int getReceiveBufferSize()
+ {
+ return receiveBufferSize;
+ }
+
+ public void setReceiveBufferSize(int receiveBufferSize)
+ {
+ this.receiveBufferSize = receiveBufferSize;
+ }
+
+ public boolean isReuseAddress()
+ {
+ return reuseAddress;
+ }
+
+ public void setReuseAddress(boolean reuseAddress)
+ {
+ this.reuseAddress = reuseAddress;
+ }
+
+ public void setPerformancePreferences(int connectionTime, int latency, int bandwidth)
+ {
+ // ignore
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelFactory.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelFactory.java
new file mode 100644
index 0000000000..1da5623cc9
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeServerSocketChannelFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelSink;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeServerSocketChannelFactory implements ServerSocketChannelFactory
+{
+
+ public ChannelSink sink = new FakeChannelSink();
+
+ public FakeServerSocketChannel createdChannel;
+
+ public ServerSocketChannel newChannel(ChannelPipeline pipeline)
+ {
+ createdChannel = new FakeServerSocketChannel(this, pipeline, sink);
+ return createdChannel;
+ }
+
+ public void releaseExternalResources()
+ {
+ // nothing to do
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeSocketChannel.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeSocketChannel.java
new file mode 100644
index 0000000000..1cc94eb760
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/FakeSocketChannel.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.channel.AbstractChannel;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelFactory;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelSink;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.SocketChannel;
+import org.jboss.netty.channel.socket.SocketChannelConfig;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class FakeSocketChannel extends AbstractChannel implements SocketChannel
+{
+
+ public InetSocketAddress localAddress;
+
+ public InetSocketAddress remoteAddress;
+
+ public SocketChannelConfig config = new FakeChannelConfig();
+
+ public boolean bound = false;
+
+ public boolean connected = false;
+
+ public ChannelSink sink;
+
+ public FakeSocketChannel(Channel parent, ChannelFactory factory, ChannelPipeline pipeline, ChannelSink sink)
+ {
+ super(parent, factory, pipeline, sink);
+ this.sink = sink;
+ }
+
+ public InetSocketAddress getLocalAddress()
+ {
+ return localAddress;
+ }
+
+ public SocketChannelConfig getConfig()
+ {
+ return config;
+ }
+
+ public InetSocketAddress getRemoteAddress()
+ {
+ return remoteAddress;
+ }
+
+ public boolean isBound()
+ {
+ return bound;
+ }
+
+ public boolean isConnected()
+ {
+ return connected;
+ }
+
+ public void emulateConnected(InetSocketAddress localAddress, InetSocketAddress remoteAddress,
+ ChannelFuture connectedFuture)
+ {
+ if (connected)
+ {
+ return;
+ }
+
+ emulateBound(localAddress, null);
+ this.remoteAddress = remoteAddress;
+ connected = true;
+ Channels.fireChannelConnected(this, remoteAddress);
+ if (connectedFuture != null)
+ {
+ connectedFuture.setSuccess();
+ }
+ }
+
+ public void emulateBound(InetSocketAddress localAddress, ChannelFuture boundFuture)
+ {
+ if (bound)
+ {
+ return;
+ }
+
+ bound = true;
+ this.localAddress = localAddress;
+ Channels.fireChannelBound(this, localAddress);
+ if (boundFuture != null)
+ {
+ boundFuture.setSuccess();
+ }
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSinkTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSinkTest.java
new file mode 100644
index 0000000000..123e85af4f
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelAcceptedChannelSinkTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelAcceptedChannelSinkTest
+{
+
+ private static final String TUNNEL_ID = "1";
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ ServerMessageSwitchDownstreamInterface messageSwitch;
+
+ private HttpTunnelAcceptedChannelSink sink;
+
+ private FakeSocketChannel channel;
+
+ private UpstreamEventCatcher upstreamCatcher;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ messageSwitch = mockContext.mock(ServerMessageSwitchDownstreamInterface.class);
+ sink = new HttpTunnelAcceptedChannelSink(messageSwitch, TUNNEL_ID, new HttpTunnelAcceptedChannelConfig());
+ ChannelPipeline pipeline = Channels.pipeline();
+ upstreamCatcher = new UpstreamEventCatcher();
+ pipeline.addLast(UpstreamEventCatcher.NAME, upstreamCatcher);
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ upstreamCatcher.events.clear();
+ }
+
+ @Test
+ public void testSendInvalidDataType()
+ {
+ Channels.write(channel, new Object());
+ assertEquals(1, upstreamCatcher.events.size());
+ NettyTestUtils.checkIsExceptionEvent(upstreamCatcher.events.poll());
+ }
+
+ @Test
+ public void testUnbind()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).serverCloseTunnel(TUNNEL_ID);
+ }
+ });
+ Channels.unbind(channel);
+ }
+
+ @Test
+ public void testDisconnect()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(messageSwitch).serverCloseTunnel(TUNNEL_ID);
+ }
+ });
+
+ Channels.disconnect(channel);
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfigTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfigTest.java
new file mode 100644
index 0000000000..c2308957db
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelConfigTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.jboss.netty.channel.socket.SocketChannelConfig;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelClientChannelConfigTest
+{
+
+ JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ SocketChannelConfig sendChannelConfig;
+
+ SocketChannelConfig pollChannelConfig;
+
+ HttpTunnelClientChannelConfig config;
+
+ @Before
+ public void setUp()
+ {
+ sendChannelConfig = mockContext.mock(SocketChannelConfig.class, "sendChannelConfig");
+ pollChannelConfig = mockContext.mock(SocketChannelConfig.class, "pollChannelConfig");
+
+ config = new HttpTunnelClientChannelConfig(sendChannelConfig, pollChannelConfig);
+ }
+
+ @Test
+ public void testGetReceiveBufferSize()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).getReceiveBufferSize();
+ will(returnValue(100));
+ }
+ });
+
+ assertEquals(100, config.getReceiveBufferSize());
+ }
+
+ @Test
+ public void testGetSendBufferSize()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).getSendBufferSize();
+ will(returnValue(100));
+ }
+ });
+
+ assertEquals(100, config.getSendBufferSize());
+ }
+
+ @Test
+ public void testGetSoLinger()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).getSoLinger();
+ will(returnValue(100));
+ }
+ });
+
+ assertEquals(100, config.getSoLinger());
+ }
+
+ @Test
+ public void testTrafficClass()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).getTrafficClass();
+ will(returnValue(1));
+ }
+ });
+
+ assertEquals(1, config.getTrafficClass());
+ }
+
+ @Test
+ public void testIsKeepAlive()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).isKeepAlive();
+ will(returnValue(true));
+ }
+ });
+
+ assertTrue(config.isKeepAlive());
+ }
+
+ @Test
+ public void testIsReuseAddress()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).isReuseAddress();
+ will(returnValue(true));
+ }
+ });
+
+ assertTrue(config.isReuseAddress());
+ }
+
+ @Test
+ public void testIsTcpNoDelay()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).isTcpNoDelay();
+ will(returnValue(true));
+ }
+ });
+
+ assertTrue(config.isTcpNoDelay());
+ }
+
+ @Test
+ public void testSetKeepAlive()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setKeepAlive(true);
+ one(sendChannelConfig).setKeepAlive(true);
+ }
+ });
+
+ config.setKeepAlive(true);
+ }
+
+ @Test
+ public void testSetPerformancePreferences()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setPerformancePreferences(100, 200, 300);
+ one(sendChannelConfig).setPerformancePreferences(100, 200, 300);
+ }
+ });
+
+ config.setPerformancePreferences(100, 200, 300);
+ }
+
+ @Test
+ public void testSetReceiveBufferSize()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setReceiveBufferSize(100);
+ one(sendChannelConfig).setReceiveBufferSize(100);
+ }
+ });
+
+ config.setReceiveBufferSize(100);
+ }
+
+ @Test
+ public void testSetReuseAddress()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setReuseAddress(true);
+ one(sendChannelConfig).setReuseAddress(true);
+ }
+ });
+
+ config.setReuseAddress(true);
+ }
+
+ @Test
+ public void testSetSendBufferSize()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setSendBufferSize(100);
+ one(sendChannelConfig).setSendBufferSize(100);
+ }
+ });
+
+ config.setSendBufferSize(100);
+ }
+
+ @Test
+ public void testSetSoLinger()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setSoLinger(100);
+ one(sendChannelConfig).setSoLinger(100);
+ }
+ });
+
+ config.setSoLinger(100);
+ }
+
+ @Test
+ public void testTcpNoDelay()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setTcpNoDelay(true);
+ one(sendChannelConfig).setTcpNoDelay(true);
+ }
+ });
+
+ config.setTcpNoDelay(true);
+ }
+
+ @Test
+ public void testSetTrafficClass()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(pollChannelConfig).setTrafficClass(1);
+ one(sendChannelConfig).setTrafficClass(1);
+ }
+ });
+
+ config.setTrafficClass(1);
+ }
+
+ @Test
+ public void testSetHighWaterMark()
+ {
+ config.setWriteBufferHighWaterMark(128 * 1024);
+ assertEquals(128 * 1024, config.getWriteBufferHighWaterMark());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetHighWaterMark_negative()
+ {
+ config.setWriteBufferHighWaterMark(-1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetHighWaterMark_zero()
+ {
+ config.setWriteBufferHighWaterMark(0);
+ }
+
+ @Test
+ public void testSetLowWaterMark()
+ {
+ config.setWriteBufferLowWaterMark(100);
+ assertEquals(100, config.getWriteBufferLowWaterMark());
+ }
+
+ @Test
+ public void testSetLowWaterMark_zero()
+ {
+ // zero is permitted for the low water mark, unlike high water mark
+ config.setWriteBufferLowWaterMark(0);
+ assertEquals(0, config.getWriteBufferLowWaterMark());
+ }
+
+ @Test
+ public void testSetHighWaterMark_lowerThanLow()
+ {
+ config.setWriteBufferLowWaterMark(100);
+ try
+ {
+ config.setWriteBufferHighWaterMark(80);
+ fail("expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertEquals("Write buffer high water mark must be strictly greater than the low water mark", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testSetLowWaterMark_higherThanHigh()
+ {
+ config.setWriteBufferHighWaterMark(128 * 1024);
+ try
+ {
+ config.setWriteBufferLowWaterMark(256 * 1024);
+ fail("expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException e)
+ {
+ assertEquals("Write buffer low water mark must be strictly less than the high water mark", e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelTest.java
new file mode 100644
index 0000000000..a7ed487015
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientChannelTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelClientChannelTest
+{
+
+ public static final int LOCAL_PORT = 50123;
+
+ /** used to emulate the selection of a random port in response to a bind request
+ * on an ephemeral port.
+ */
+ public static final int OTHER_LOCAL_PORT = 40652;
+
+ public static final InetSocketAddress LOCAL_ADDRESS = InetSocketAddress.createUnresolved("localhost", LOCAL_PORT);
+
+ public static final InetSocketAddress LOCAL_ADDRESS_EPHEMERAL_PORT = InetSocketAddress.createUnresolved("localhost",
+ 0);
+
+ public static final InetSocketAddress REMOTE_ADDRESS = InetSocketAddress.createUnresolved("test.server.com", 12345);
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV4;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV6;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV4_EPHEMERAL_PORT;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT;
+
+ public static final InetSocketAddress RESOLVED_LOCAL_ADDRESS_IPV6_SELECTED_PORT;
+
+ static
+ {
+ try
+ {
+ InetAddress localhostIPV4 = InetAddress.getByAddress(new byte[]
+ {127, 0, 0, 1});
+ InetAddress localhostIPV6 = InetAddress.getByAddress(new byte[]
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1});
+ RESOLVED_LOCAL_ADDRESS_IPV4 = new InetSocketAddress(localhostIPV4, LOCAL_PORT);
+ RESOLVED_LOCAL_ADDRESS_IPV6 = new InetSocketAddress(localhostIPV6, LOCAL_PORT);
+ RESOLVED_LOCAL_ADDRESS_IPV4_EPHEMERAL_PORT = new InetSocketAddress(localhostIPV4, 0);
+ RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT = new InetSocketAddress(localhostIPV6, 0);
+ RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT = new InetSocketAddress(localhostIPV4, OTHER_LOCAL_PORT);
+ RESOLVED_LOCAL_ADDRESS_IPV6_SELECTED_PORT = new InetSocketAddress(localhostIPV6, OTHER_LOCAL_PORT);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new RuntimeException(
+ "Creation of InetAddresses should not fail when explicitly specified and the correct length", e);
+ }
+ }
+
+ private UpstreamEventCatcher upstreamCatcher;
+
+ private HttpTunnelClientChannel channel;
+
+ private FakeClientSocketChannelFactory outboundFactory;
+
+ private FakeSocketChannel sendChannel;
+
+ private FakeSocketChannel pollChannel;
+
+ private FakeChannelSink sendSink;
+
+ private FakeChannelSink pollSink;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ upstreamCatcher = new UpstreamEventCatcher();
+ pipeline.addLast(UpstreamEventCatcher.NAME, upstreamCatcher);
+
+ outboundFactory = new FakeClientSocketChannelFactory();
+
+ HttpTunnelClientChannelFactory factory = new HttpTunnelClientChannelFactory(outboundFactory);
+ channel = factory.newChannel(pipeline);
+
+ assertEquals(2, outboundFactory.createdChannels.size());
+
+ sendChannel = outboundFactory.createdChannels.get(0);
+ pollChannel = outboundFactory.createdChannels.get(1);
+ sendSink = (FakeChannelSink) sendChannel.sink;
+ pollSink = (FakeChannelSink) pollChannel.sink;
+ }
+
+ @Test
+ public void testConnect()
+ {
+ Channels.connect(channel, REMOTE_ADDRESS);
+
+ // this should result in a CONNECTED state event on the send channel, but not on the poll
+ // channel just yet
+ assertEquals(1, sendSink.events.size());
+ assertEquals(0, pollSink.events.size());
+ ChannelEvent sendChannelEvent = sendSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(sendChannelEvent, ChannelState.CONNECTED, REMOTE_ADDRESS);
+
+ // once the send channel indicates that it is connected, we should see the tunnel open request
+ // being sent
+ sendChannel.emulateConnected(LOCAL_ADDRESS, REMOTE_ADDRESS, ((ChannelStateEvent) sendChannelEvent).getFuture());
+ assertEquals(1, sendSink.events.size());
+ ChannelEvent openTunnelRequest = sendSink.events.poll();
+ NettyTestUtils.checkIsDownstreamMessageEvent(openTunnelRequest, ChannelBuffer.class);
+ }
+
+ @Test
+ public void testBind_unresolvedAddress()
+ {
+ // requesting a binding with an unresolved local address
+ // should attempt to bind the send channel with that address unaltered
+ // and attempt to bind the poll address with the same host name but
+ // an ephemeral port. We emulate a resolved IPV4 address for the bind
+ // response.
+ checkBinding(LOCAL_ADDRESS, LOCAL_ADDRESS, LOCAL_ADDRESS_EPHEMERAL_PORT, RESOLVED_LOCAL_ADDRESS_IPV4,
+ RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT);
+ }
+
+ @Test
+ public void testBind_resolvedAddress_ipv4()
+ {
+ // variant that uses resolved addresses. The bind request
+ // for the poll channel should also use a resolved address,
+ // built from the provided resolved address.
+ checkBinding(RESOLVED_LOCAL_ADDRESS_IPV4, RESOLVED_LOCAL_ADDRESS_IPV4,
+ RESOLVED_LOCAL_ADDRESS_IPV4_EPHEMERAL_PORT, RESOLVED_LOCAL_ADDRESS_IPV4,
+ RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT);
+ }
+
+ @Test
+ public void testBind_resolvedAddress_ipv6()
+ {
+ // variant that uses a resolved IPV6 address.
+ // bind request on the poll channel should use the same
+ // IPv6 host, with an ephemeral port.
+ checkBinding(RESOLVED_LOCAL_ADDRESS_IPV6, RESOLVED_LOCAL_ADDRESS_IPV6,
+ RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT, RESOLVED_LOCAL_ADDRESS_IPV6,
+ RESOLVED_LOCAL_ADDRESS_IPV6_SELECTED_PORT);
+ }
+
+ private void checkBinding(InetSocketAddress requestedBindAddress, InetSocketAddress expectedPollBindRequest,
+ InetSocketAddress expectedSendBindRequest, InetSocketAddress emulatedPollBindAddress,
+ InetSocketAddress emulatedSendBindAddress)
+ {
+
+ ChannelFuture bindFuture = Channels.bind(channel, requestedBindAddress);
+ assertFalse(bindFuture.isDone());
+
+ assertEquals(1, sendSink.events.size());
+ assertEquals(1, pollSink.events.size());
+
+ ChannelEvent sendChannelEvent = sendSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(sendChannelEvent, ChannelState.BOUND, expectedPollBindRequest);
+ ChannelEvent pollChannelEvent = pollSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(pollChannelEvent, ChannelState.BOUND, expectedSendBindRequest);
+
+ sendChannel.emulateBound(emulatedPollBindAddress, sendChannelEvent.getFuture());
+ assertFalse(bindFuture.isDone());
+ pollChannel.emulateBound(emulatedSendBindAddress, pollChannelEvent.getFuture());
+ assertTrue(bindFuture.isDone());
+ assertTrue(bindFuture.isSuccess());
+
+ assertEquals(channel.getLocalAddress(), emulatedPollBindAddress);
+ }
+
+ @Test
+ public void testBind_preResolvedAddress_ipv6()
+ {
+ ChannelFuture bindFuture = Channels.bind(channel, RESOLVED_LOCAL_ADDRESS_IPV6);
+ assertFalse(bindFuture.isDone());
+
+ assertEquals(1, sendSink.events.size());
+ assertEquals(1, pollSink.events.size());
+
+ ChannelEvent sendChannelEvent = sendSink.events.poll();
+ NettyTestUtils.checkIsStateEvent(sendChannelEvent, ChannelState.BOUND, RESOLVED_LOCAL_ADDRESS_IPV6);
+ ChannelEvent pollChannelEvent = pollSink.events.poll();
+ NettyTestUtils
+ .checkIsStateEvent(pollChannelEvent, ChannelState.BOUND, RESOLVED_LOCAL_ADDRESS_IPV6_EPHEMERAL_PORT);
+
+ sendChannel.emulateBound(RESOLVED_LOCAL_ADDRESS_IPV6, sendChannelEvent.getFuture());
+ assertFalse(bindFuture.isDone());
+ pollChannel.emulateBound(RESOLVED_LOCAL_ADDRESS_IPV4_SELECTED_PORT, pollChannelEvent.getFuture());
+ assertTrue(bindFuture.isDone());
+ assertTrue(bindFuture.isSuccess());
+
+ assertEquals(channel.getLocalAddress(), RESOLVED_LOCAL_ADDRESS_IPV6);
+ }
+
+ @Test
+ public void testBind_sendBindFails()
+ {
+ ChannelFuture bindFuture = Channels.bind(channel, LOCAL_ADDRESS);
+ assertFalse(bindFuture.isDone());
+
+ Exception bindFailureReason = new Exception("could not bind");
+ ((ChannelStateEvent) sendSink.events.poll()).getFuture().setFailure(bindFailureReason);
+ assertTrue(bindFuture.isDone());
+ assertFalse(bindFuture.isSuccess());
+ assertSame(bindFailureReason, bindFuture.getCause());
+ }
+
+ @Test
+ public void testBind_pollBindFails()
+ {
+ ChannelFuture bindFuture = Channels.bind(channel, LOCAL_ADDRESS);
+ assertFalse(bindFuture.isDone());
+
+ Exception bindFailureReason = new Exception("could not bind");
+ ((ChannelStateEvent) pollSink.events.poll()).getFuture().setFailure(bindFailureReason);
+ assertTrue(bindFuture.isDone());
+ assertFalse(bindFuture.isSuccess());
+ assertSame(bindFailureReason, bindFuture.getCause());
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandlerTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandlerTest.java
new file mode 100644
index 0000000000..3db51162cc
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientPollHandlerTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.DownstreamMessageEvent;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelClientPollHandlerTest
+{
+
+ private static final String TUNNEL_ID = "1";
+
+ private static final InetSocketAddress SERVER_ADDRESS = createAddress(new byte[]
+ {10, 0, 0, 3}, 12345);
+
+ private static final InetSocketAddress PROXY_ADDRESS = createAddress(new byte[]
+ {10, 0, 0, 2}, 8888);
+
+ private static final InetSocketAddress LOCAL_ADDRESS = createAddress(new byte[]
+ {10, 0, 0, 1}, 54321);
+
+ private FakeSocketChannel channel;
+
+ private FakeChannelSink sink;
+
+ private HttpTunnelClientPollHandler handler;
+
+ private MockChannelStateListener listener;
+
+ private static InetSocketAddress createAddress(byte[] addr, int port)
+ {
+ try
+ {
+ return new InetSocketAddress(InetAddress.getByAddress(addr), port);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new RuntimeException("Bad address in test");
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception
+ {
+ sink = new FakeChannelSink();
+
+ ChannelPipeline pipeline = Channels.pipeline();
+ listener = new MockChannelStateListener();
+ listener.serverHostName = HttpTunnelMessageUtils.convertToHostString(SERVER_ADDRESS);
+ handler = new HttpTunnelClientPollHandler(listener);
+ handler.setTunnelId(TUNNEL_ID);
+ pipeline.addLast(HttpTunnelClientPollHandler.NAME, handler);
+
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ channel.remoteAddress = PROXY_ADDRESS;
+ channel.localAddress = LOCAL_ADDRESS;
+ }
+
+ @Test
+ public void testSendsRequestOnConnect()
+ {
+ Channels.fireChannelConnected(channel, PROXY_ADDRESS);
+ assertEquals(1, sink.events.size());
+ HttpRequest request = checkIsMessageEventContainingHttpRequest(sink.events.poll());
+ assertTrue(HttpTunnelMessageUtils.isServerToClientRequest(request));
+ assertTrue(HttpTunnelMessageUtils.checkHost(request, SERVER_ADDRESS));
+ assertTrue(listener.fullyEstablished);
+ }
+
+ @Test
+ public void testSendsReceivedDataSentUpstream()
+ {
+ HttpResponse response = HttpTunnelMessageUtils.createRecvDataResponse(NettyTestUtils.createData(1234L));
+ Channels.fireMessageReceived(channel, response);
+ assertEquals(1, listener.messages.size());
+ assertEquals(1234L, listener.messages.get(0).readLong());
+ }
+
+ @Test
+ public void testSendsAnotherRequestAfterResponse()
+ {
+ HttpResponse response = HttpTunnelMessageUtils.createRecvDataResponse(NettyTestUtils.createData(1234L));
+ Channels.fireMessageReceived(channel, response);
+ assertEquals(1, sink.events.size());
+ checkIsMessageEventContainingHttpRequest(sink.events.poll());
+ }
+
+ private HttpRequest checkIsMessageEventContainingHttpRequest(ChannelEvent event)
+ {
+ assertTrue(event instanceof DownstreamMessageEvent);
+ DownstreamMessageEvent messageEvent = (DownstreamMessageEvent) event;
+ assertTrue(messageEvent.getMessage() instanceof HttpRequest);
+ return (HttpRequest) messageEvent.getMessage();
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandlerTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandlerTest.java
new file mode 100644
index 0000000000..361d143f88
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelClientSendHandlerTest.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.DownstreamMessageEvent;
+import org.jboss.netty.handler.codec.http.HttpHeaders;
+import org.jboss.netty.handler.codec.http.HttpRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelClientSendHandlerTest
+{
+
+ private static final InetSocketAddress SERVER_ADDRESS = createAddress(new byte[]
+ {10, 0, 0, 3}, 12345);
+
+ private static final InetSocketAddress PROXY_ADDRESS = createAddress(new byte[]
+ {10, 0, 0, 2}, 8888);
+
+ private static final InetSocketAddress LOCAL_ADDRESS = createAddress(new byte[]
+ {10, 0, 0, 1}, 54321);
+
+ private FakeSocketChannel channel;
+
+ private FakeChannelSink sink;
+
+ private HttpTunnelClientSendHandler handler;
+
+ private MockChannelStateListener listener;
+
+ @Before
+ public void setUp()
+ {
+ sink = new FakeChannelSink();
+ ChannelPipeline pipeline = Channels.pipeline();
+ listener = new MockChannelStateListener();
+ listener.serverHostName = HttpTunnelMessageUtils.convertToHostString(SERVER_ADDRESS);
+ handler = new HttpTunnelClientSendHandler(listener);
+ pipeline.addLast(HttpTunnelClientSendHandler.NAME, handler);
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ channel.remoteAddress = PROXY_ADDRESS;
+ channel.localAddress = LOCAL_ADDRESS;
+ }
+
+ private static InetSocketAddress createAddress(byte[] addr, int port)
+ {
+ try
+ {
+ return new InetSocketAddress(InetAddress.getByAddress(addr), port);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new RuntimeException("Bad address in test");
+ }
+ }
+
+ @Test
+ public void testSendsTunnelOpen() throws Exception
+ {
+ Channels.fireChannelConnected(channel, PROXY_ADDRESS);
+ assertEquals(1, sink.events.size());
+ HttpRequest request = NettyTestUtils.checkIsDownstreamMessageEvent(sink.events.poll(), HttpRequest.class);
+ assertTrue(HttpTunnelMessageUtils.isOpenTunnelRequest(request));
+ assertTrue(HttpTunnelMessageUtils.checkHost(request, SERVER_ADDRESS));
+ }
+
+ @Test
+ public void testStoresTunnelId() throws Exception
+ {
+ emulateConnect();
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createTunnelOpenResponse("newTunnel"));
+ assertEquals("newTunnel", handler.getTunnelId());
+ assertEquals("newTunnel", listener.tunnelId);
+ }
+
+ @Test
+ public void testSendData()
+ {
+ emulateConnectAndOpen();
+ channel.write(NettyTestUtils.createData(1234L));
+ assertEquals(1, sink.events.size());
+ ChannelEvent sentEvent = sink.events.poll();
+ checkIsSendDataRequestWithData(sentEvent, NettyTestUtils.createData(1234L));
+ }
+
+ @Test
+ public void testWillNotSendDataUntilTunnelIdSet()
+ {
+ emulateConnect();
+ channel.write(NettyTestUtils.createData(1234L));
+
+ assertEquals(0, sink.events.size());
+
+ Channels.fireChannelConnected(channel, PROXY_ADDRESS);
+ assertEquals(1, sink.events.size());
+ }
+
+ @Test
+ public void testOnlyOneRequestAtATime()
+ {
+ emulateConnectAndOpen();
+
+ channel.write(NettyTestUtils.createData(1234L));
+ assertEquals(1, sink.events.size());
+ checkIsSendDataRequestWithData(sink.events.poll(), NettyTestUtils.createData(1234L));
+
+ channel.write(NettyTestUtils.createData(5678L));
+ assertEquals(0, sink.events.size());
+
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createSendDataResponse());
+ assertEquals(1, sink.events.size());
+ checkIsSendDataRequestWithData(sink.events.poll(), NettyTestUtils.createData(5678L));
+ }
+
+ @Test
+ public void testDisconnect()
+ {
+ emulateConnectAndOpen();
+
+ channel.write(NettyTestUtils.createData(1234L));
+ assertEquals(1, sink.events.size());
+ checkIsSendDataRequestWithData(sink.events.poll(), NettyTestUtils.createData(1234L));
+
+ channel.disconnect();
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createSendDataResponse());
+ assertEquals(1, sink.events.size());
+
+ HttpRequest request = NettyTestUtils.checkIsDownstreamMessageEvent(sink.events.poll(), HttpRequest.class);
+ assertTrue(HttpTunnelMessageUtils.isCloseTunnelRequest(request));
+ assertEquals("newTunnel", HttpTunnelMessageUtils.extractTunnelId(request));
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createTunnelCloseResponse());
+ assertEquals(1, sink.events.size());
+ NettyTestUtils.checkIsStateEvent(sink.events.poll(), ChannelState.CONNECTED, null);
+ }
+
+ @Test
+ public void testClose()
+ {
+ emulateConnectAndOpen();
+
+ channel.close();
+ assertEquals(1, sink.events.size());
+ HttpRequest request = NettyTestUtils.checkIsDownstreamMessageEvent(sink.events.poll(), HttpRequest.class);
+ assertTrue(HttpTunnelMessageUtils.isCloseTunnelRequest(request));
+ assertEquals("newTunnel", HttpTunnelMessageUtils.extractTunnelId(request));
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createTunnelCloseResponse());
+ assertEquals(1, sink.events.size());
+ NettyTestUtils.checkIsStateEvent(sink.events.poll(), ChannelState.OPEN, false);
+ }
+
+ @Test
+ public void testWritesAfterCloseAreRejected()
+ {
+ emulateConnectAndOpen();
+
+ channel.close();
+ assertFalse(channel.write(NettyTestUtils.createData(1234L)).isSuccess());
+ }
+
+ private void checkIsSendDataRequestWithData(ChannelEvent event, ChannelBuffer data)
+ {
+ assertTrue(event instanceof DownstreamMessageEvent);
+ DownstreamMessageEvent messageEvent = (DownstreamMessageEvent) event;
+ assertTrue(messageEvent.getMessage() instanceof HttpRequest);
+ HttpRequest request = (HttpRequest) messageEvent.getMessage();
+ assertTrue(HttpTunnelMessageUtils.isSendDataRequest(request));
+ assertEquals(data.readableBytes(), HttpHeaders.getContentLength(request));
+
+ ChannelBuffer content = request.getContent();
+ NettyTestUtils.assertEquals(data, content);
+ }
+
+ private void emulateConnect()
+ {
+ channel.emulateConnected(LOCAL_ADDRESS, PROXY_ADDRESS, null);
+ sink.events.clear();
+ }
+
+ private void emulateConnectAndOpen()
+ {
+ emulateConnect();
+ Channels.fireMessageReceived(channel, HttpTunnelMessageUtils.createTunnelOpenResponse("newTunnel"));
+
+ sink.events.clear();
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactoryTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactoryTest.java
new file mode 100644
index 0000000000..1483b86725
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelFactoryTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import org.jboss.netty.channel.ChannelException;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelServerChannelFactoryTest
+{
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ ServerSocketChannelFactory realChannelFactory;
+
+ private HttpTunnelServerChannelFactory factory;
+
+ ServerSocketChannel realChannel;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ realChannelFactory = mockContext.mock(ServerSocketChannelFactory.class);
+ factory = new HttpTunnelServerChannelFactory(realChannelFactory);
+ ChannelPipeline pipeline = Channels.pipeline();
+ realChannel = new FakeServerSocketChannel(factory, pipeline, new FakeChannelSink());
+ }
+
+ @Test
+ public void testNewChannel()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(realChannelFactory).newChannel(with(any(ChannelPipeline.class)));
+ will(returnValue(realChannel));
+ }
+ });
+ ChannelPipeline pipeline = Channels.pipeline();
+ HttpTunnelServerChannel newChannel = factory.newChannel(pipeline);
+ assertNotNull(newChannel);
+ assertSame(pipeline, newChannel.getPipeline());
+ }
+
+ @Test
+ public void testNewChannel_forwardsWrappedFactoryFailure()
+ {
+ final ChannelException innerException = new ChannelException();
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(realChannelFactory).newChannel(with(any(ChannelPipeline.class)));
+ will(throwException(innerException));
+ }
+ });
+
+ try
+ {
+ factory.newChannel(Channels.pipeline());
+ fail("Expected ChannelException");
+ }
+ catch (ChannelException e)
+ {
+ assertSame(innerException, e);
+ }
+ }
+
+ // @Test
+ // public void testChannelCreation_withServerBootstrap() {
+ // mockContext.checking(new Expectations() {{
+ // one(realChannelFactory).newChannel(with(any(ChannelPipeline.class))); will(returnValue(realChannel));
+ // }});
+ //
+ // ServerBootstrap bootstrap = new ServerBootstrap(factory);
+ // Channel newChannel = bootstrap.bind(new InetSocketAddress(80));
+ // assertNotNull(newChannel);
+ //
+ // }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSinkTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSinkTest.java
new file mode 100644
index 0000000000..2beee03e9c
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelSinkTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.ExceptionEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.socket.ServerSocketChannel;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelServerChannelSinkTest
+{
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private HttpTunnelServerChannelSink sink;
+
+ private ChannelPipeline pipeline;
+
+ private FakeSocketChannel channel;
+
+ ServerSocketChannel realChannel;
+
+ ChannelFuture realFuture;
+
+ Throwable exceptionInPipeline;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ realChannel = mockContext.mock(ServerSocketChannel.class);
+ pipeline = Channels.pipeline();
+ pipeline.addLast("exceptioncatcher", new ExceptionCatcher());
+ sink = new HttpTunnelServerChannelSink();
+ sink.setRealChannel(realChannel);
+ channel = new FakeSocketChannel(null, null, pipeline, sink);
+ realFuture = Channels.future(realChannel);
+ }
+
+ @After
+ public void teardown() throws Exception
+ {
+ assertTrue("exception caught in pipeline: " + exceptionInPipeline, exceptionInPipeline == null);
+ }
+
+ public void testCloseRequest() throws Exception
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(realChannel).close();
+ will(returnValue(realFuture));
+ }
+ });
+
+ ChannelFuture virtualFuture1 = Channels.close(channel);
+ mockContext.assertIsSatisfied();
+ ChannelFuture virtualFuture = virtualFuture1;
+ realFuture.setSuccess();
+ assertTrue(virtualFuture.isSuccess());
+ }
+
+ @Test
+ public void testUnbindRequest_withSuccess() throws Exception
+ {
+ ChannelFuture virtualFuture = checkUnbind();
+ realFuture.setSuccess();
+ assertTrue(virtualFuture.isSuccess());
+ }
+
+ @Test
+ public void testUnbindRequest_withFailure() throws Exception
+ {
+ ChannelFuture virtualFuture = checkUnbind();
+ realFuture.setFailure(new Exception("Something bad happened"));
+ assertFalse(virtualFuture.isSuccess());
+ }
+
+ private ChannelFuture checkUnbind()
+ {
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(realChannel).unbind();
+ will(returnValue(realFuture));
+ }
+ });
+
+ ChannelFuture virtualFuture = Channels.unbind(channel);
+ mockContext.assertIsSatisfied();
+ return virtualFuture;
+ }
+
+ @Test
+ public void testBindRequest_withSuccess()
+ {
+ ChannelFuture virtualFuture = checkBind();
+ realFuture.setSuccess();
+ assertTrue(virtualFuture.isSuccess());
+ }
+
+ @Test
+ public void testBindRequest_withFailure()
+ {
+ ChannelFuture virtualFuture = checkBind();
+ realFuture.setFailure(new Exception("Something bad happened"));
+ assertFalse(virtualFuture.isSuccess());
+ }
+
+ private ChannelFuture checkBind()
+ {
+ final SocketAddress toAddress = new InetSocketAddress(80);
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(realChannel).bind(toAddress);
+ will(returnValue(realFuture));
+ }
+ });
+
+ ChannelFuture virtualFuture = Channels.bind(channel, toAddress);
+ return virtualFuture;
+ }
+
+ private final class ExceptionCatcher extends SimpleChannelUpstreamHandler
+ {
+
+ ExceptionCatcher()
+ {
+ super();
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception
+ {
+ exceptionInPipeline = e.getCause();
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelTest.java
new file mode 100644
index 0000000000..bbe8d63218
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelServerChannelTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.UpstreamChannelStateEvent;
+import org.jboss.netty.channel.socket.ServerSocketChannelConfig;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class HttpTunnelServerChannelTest
+{
+
+ JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private HttpTunnelServerChannel virtualChannel;
+
+ private UpstreamEventCatcher upstreamEvents;
+
+ private FakeServerSocketChannelFactory realChannelFactory;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ realChannelFactory = new FakeServerSocketChannelFactory();
+ realChannelFactory.sink = new FakeChannelSink();
+
+ HttpTunnelServerChannelFactory factory = new HttpTunnelServerChannelFactory(realChannelFactory);
+ virtualChannel = factory.newChannel(createVirtualChannelPipeline());
+ }
+
+ private ChannelPipeline createVirtualChannelPipeline()
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ upstreamEvents = new UpstreamEventCatcher();
+ pipeline.addLast(UpstreamEventCatcher.NAME, upstreamEvents);
+ return pipeline;
+ }
+
+ @Test
+ public void testGetLocalAddress_delegatedToRealChannel()
+ {
+ realChannelFactory.createdChannel.localAddress = InetSocketAddress.createUnresolved("mycomputer", 80);
+ SocketAddress returned = virtualChannel.getLocalAddress();
+ assertSame(realChannelFactory.createdChannel.localAddress, returned);
+ }
+
+ @Test
+ public void testGetRemoteAddress_returnsNull()
+ {
+ assertNull(virtualChannel.getRemoteAddress());
+ }
+
+ @Test
+ public void testIsBound_delegatedToRealChannel()
+ {
+ realChannelFactory.createdChannel.bound = true;
+ assertTrue(virtualChannel.isBound());
+ realChannelFactory.createdChannel.bound = false;
+ assertFalse(virtualChannel.isBound());
+ }
+
+ @Test
+ public void testConstruction_firesOpenEvent()
+ {
+ assertTrue(upstreamEvents.events.size() > 0);
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(), virtualChannel, ChannelState.OPEN, Boolean.TRUE);
+ }
+
+ @Test
+ public void testChannelBoundEventFromReal_replicatedOnVirtual()
+ {
+ upstreamEvents.events.clear();
+ InetSocketAddress boundAddr = InetSocketAddress.createUnresolved("mycomputer", 12345);
+ Channels.fireChannelBound(realChannelFactory.createdChannel, boundAddr);
+ assertEquals(1, upstreamEvents.events.size());
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(), virtualChannel, ChannelState.BOUND, boundAddr);
+ }
+
+ @Test
+ public void testChannelUnboundEventFromReal_replicatedOnVirtual()
+ {
+ upstreamEvents.events.clear();
+ Channels.fireChannelUnbound(realChannelFactory.createdChannel);
+ assertEquals(1, upstreamEvents.events.size());
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(), virtualChannel, ChannelState.BOUND, null);
+ }
+
+ @Test
+ public void testChannelClosedEventFromReal_replicatedOnVirtual()
+ {
+ upstreamEvents.events.clear();
+ Channels.fireChannelClosed(realChannelFactory.createdChannel);
+ assertEquals(1, upstreamEvents.events.size());
+ checkIsUpstreamChannelStateEvent(upstreamEvents.events.poll(), virtualChannel, ChannelState.OPEN, Boolean.FALSE);
+ }
+
+ @Test
+ public void testHasConfiguration()
+ {
+ assertNotNull(virtualChannel.getConfig());
+ }
+
+ @Test
+ public void testChangePipelineFactoryDoesNotAffectRealChannel()
+ {
+ ChannelPipelineFactory realPipelineFactory = realChannelFactory.createdChannel.getConfig().getPipelineFactory();
+ ChannelPipelineFactory virtualPipelineFactory = mockContext.mock(ChannelPipelineFactory.class);
+ virtualChannel.getConfig().setPipelineFactory(virtualPipelineFactory);
+ assertSame(virtualPipelineFactory, virtualChannel.getConfig().getPipelineFactory());
+
+ // channel pipeline factory is a special case: we do not want it set on the configuration
+ // of the underlying factory
+ assertSame(realPipelineFactory, realChannelFactory.createdChannel.getConfig().getPipelineFactory());
+ }
+
+ @Test
+ public void testChangingBacklogAffectsRealChannel()
+ {
+ virtualChannel.getConfig().setBacklog(1234);
+ assertEquals(1234, realChannelFactory.createdChannel.getConfig().getBacklog());
+ }
+
+ @Test
+ public void testChangingConnectTimeoutMillisAffectsRealChannel()
+ {
+ virtualChannel.getConfig().setConnectTimeoutMillis(54321);
+ assertEquals(54321, realChannelFactory.createdChannel.getConfig().getConnectTimeoutMillis());
+ }
+
+ @Test
+ public void testChangingPerformancePreferencesAffectsRealChannel()
+ {
+ final ServerSocketChannelConfig mockConfig = mockContext.mock(ServerSocketChannelConfig.class);
+ realChannelFactory.createdChannel.config = mockConfig;
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(mockConfig).setPerformancePreferences(100, 200, 300);
+ }
+ });
+ virtualChannel.getConfig().setPerformancePreferences(100, 200, 300);
+ mockContext.assertIsSatisfied();
+ }
+
+ @Test
+ public void testChangingReceiveBufferSizeAffectsRealChannel()
+ {
+ virtualChannel.getConfig().setReceiveBufferSize(10101);
+ assertEquals(10101, realChannelFactory.createdChannel.getConfig().getReceiveBufferSize());
+ }
+
+ @Test
+ public void testChangingReuseAddressAffectsRealChannel()
+ {
+ virtualChannel.getConfig().setReuseAddress(true);
+ assertEquals(true, realChannelFactory.createdChannel.getConfig().isReuseAddress());
+ }
+
+ @Test
+ public void testSetChannelPipelineFactoryViaOption()
+ {
+ final ServerSocketChannelConfig mockConfig = mockContext.mock(ServerSocketChannelConfig.class);
+ realChannelFactory.createdChannel.config = mockConfig;
+
+ mockContext.checking(new Expectations()
+ {
+ {
+ never(mockConfig);
+ }
+ });
+
+ ChannelPipelineFactory factory = mockContext.mock(ChannelPipelineFactory.class);
+ virtualChannel.getConfig().setOption("pipelineFactory", factory);
+ assertSame(factory, virtualChannel.getConfig().getPipelineFactory());
+ }
+
+ @Test
+ public void testSetOptionAffectsRealChannel()
+ {
+ final ServerSocketChannelConfig mockConfig = mockContext.mock(ServerSocketChannelConfig.class);
+ realChannelFactory.createdChannel.config = mockConfig;
+
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(mockConfig).setOption("testOption", "testValue");
+ }
+ });
+
+ virtualChannel.getConfig().setOption("testOption", "testValue");
+ }
+
+ private void checkIsUpstreamChannelStateEvent(ChannelEvent ev, Channel expectedChannel, ChannelState expectedState,
+ Object expectedValue)
+ {
+ assertTrue(ev instanceof UpstreamChannelStateEvent);
+ UpstreamChannelStateEvent checkedEv = (UpstreamChannelStateEvent) ev;
+ assertSame(expectedChannel, checkedEv.getChannel());
+ assertEquals(expectedState, checkedEv.getState());
+ assertEquals(expectedValue, checkedEv.getValue());
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelSoakTester.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelSoakTester.java
new file mode 100644
index 0000000000..034c6e9488
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelSoakTester.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jboss.netty.bootstrap.ClientBootstrap;
+import org.jboss.netty.bootstrap.ServerBootstrap;
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+import org.jboss.netty.channel.group.DefaultChannelGroup;
+import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.ServerSocketChannelFactory;
+import org.jboss.netty.channel.socket.SocketChannel;
+import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelSoakTester
+{
+
+ private static final int SERVER_PORT = 20100;
+
+ static final Logger LOG = Logger.getLogger(HttpTunnelSoakTester.class.getName());
+
+ private static final long BYTES_TO_SEND = 1024 * 1024 * 1024;
+
+ private static final int MAX_WRITE_SIZE = 64 * 1024;
+
+ private final ServerBootstrap serverBootstrap;
+
+ private final ClientBootstrap clientBootstrap;
+
+ final ChannelGroup channels;
+
+ private final ExecutorService executor;
+
+ final ScheduledExecutorService scheduledExecutor;
+
+ final DataSender c2sDataSender = new DataSender("C2S");
+ final DataSender s2cDataSender = new DataSender("S2C");
+
+ private DataVerifier c2sVerifier = new DataVerifier("C2S-Verifier");
+ private DataVerifier s2cVerifier = new DataVerifier("S2C-Verifier");
+
+ private static byte[] SEND_STREAM;
+
+ static {
+ SEND_STREAM = new byte[MAX_WRITE_SIZE + 127];
+ for(int i=0; i < SEND_STREAM.length; i++) {
+ SEND_STREAM[i] = (byte)(i % 127);
+ }
+ }
+
+ public HttpTunnelSoakTester()
+ {
+ scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
+ executor = Executors.newCachedThreadPool();
+ ServerSocketChannelFactory serverChannelFactory = new NioServerSocketChannelFactory(executor, executor);
+ HttpTunnelServerChannelFactory serverTunnelFactory = new HttpTunnelServerChannelFactory(serverChannelFactory);
+
+ serverBootstrap = new ServerBootstrap(serverTunnelFactory);
+ serverBootstrap.setPipelineFactory(createServerPipelineFactory());
+
+ ClientSocketChannelFactory clientChannelFactory = new NioClientSocketChannelFactory(executor, executor);
+ HttpTunnelClientChannelFactory clientTunnelFactory = new HttpTunnelClientChannelFactory(clientChannelFactory);
+
+ clientBootstrap = new ClientBootstrap(clientTunnelFactory);
+ clientBootstrap.setPipelineFactory(createClientPipelineFactory());
+ configureProxy();
+
+ channels = new DefaultChannelGroup();
+ }
+
+ private void configureProxy()
+ {
+ String proxyHost = System.getProperty("http.proxyHost");
+ if (proxyHost != null && proxyHost.length() != 0)
+ {
+ int proxyPort = Integer.getInteger("http.proxyPort", 80);
+ InetAddress chosenAddress = chooseAddress(proxyHost);
+ InetSocketAddress proxyAddress = new InetSocketAddress(chosenAddress, proxyPort);
+ if (!proxyAddress.isUnresolved())
+ {
+ clientBootstrap.setOption(HttpTunnelClientChannelConfig.PROXY_ADDRESS_OPTION, proxyAddress);
+ System.out.println("Using " + proxyAddress + " as a proxy for this test run");
+ }
+ else
+ {
+ System.err.println("Failed to resolve proxy address " + proxyAddress);
+ }
+ }
+ else
+ {
+ System.out.println("No proxy specified, will connect to server directly");
+ }
+ }
+
+ private InetAddress chooseAddress(String proxyHost)
+ {
+ try
+ {
+ InetAddress[] allByName = InetAddress.getAllByName(proxyHost);
+ for (InetAddress address : allByName)
+ {
+ if (address.isAnyLocalAddress() || address.isLinkLocalAddress())
+ {
+ continue;
+ }
+
+ return address;
+ }
+
+ return null;
+ }
+ catch (UnknownHostException e)
+ {
+ return null;
+ }
+ }
+
+ protected ChannelPipelineFactory createClientPipelineFactory()
+ {
+ return new ChannelPipelineFactory()
+ {
+
+ public ChannelPipeline getPipeline() throws Exception
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("s2cVerifier", s2cVerifier);
+ pipeline.addLast("throttleControl", new SendThrottle(c2sDataSender));
+ return pipeline;
+ }
+ };
+ }
+
+ protected ChannelPipelineFactory createServerPipelineFactory()
+ {
+ return new ChannelPipelineFactory()
+ {
+
+ public ChannelPipeline getPipeline() throws Exception
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("c2sVerifier", c2sVerifier);
+ pipeline.addLast("throttleControl", new SendThrottle(s2cDataSender));
+ pipeline.addLast("sendStarter", new SimpleChannelUpstreamHandler() {
+ public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
+ Channel childChannel = e.getChannel();
+ channels.add(childChannel);
+ s2cDataSender.setChannel(childChannel);
+ executor.execute(s2cDataSender);
+ };
+ });
+ return pipeline;
+ }
+ };
+ }
+
+ public void run() throws InterruptedException
+ {
+ LOG.info("binding server channel");
+ Channel serverChannel = serverBootstrap.bind(new InetSocketAddress(SERVER_PORT));
+ channels.add(serverChannel);
+ LOG.log(Level.INFO, "server channel bound to {0}", serverChannel.getLocalAddress());
+
+ SocketChannel clientChannel = createClientChannel();
+ if (clientChannel == null)
+ {
+ LOG.severe("no client channel - bailing out");
+ return;
+ }
+
+ channels.add(clientChannel);
+ c2sDataSender.setChannel(clientChannel);
+
+ executor.execute(c2sDataSender);
+
+ if(!c2sDataSender.waitForFinish(5, TimeUnit.MINUTES)) {
+ LOG.severe("Data send from client to server failed");
+ }
+
+ if(!s2cDataSender.waitForFinish(5, TimeUnit.MINUTES)) {
+ LOG.severe("Data send from server to client failed");
+ }
+
+
+ LOG.log(Level.INFO, "Waiting for verification to complete");
+ if (!c2sVerifier.waitForCompletion(30L, TimeUnit.SECONDS))
+ {
+ LOG.warning("Timed out waiting for verification of client-to-server stream");
+ }
+
+ if (!s2cVerifier.waitForCompletion(30L, TimeUnit.SECONDS))
+ {
+ LOG.warning("Timed out waiting for verification of server-to-client stream");
+ }
+
+ LOG.info("closing client channel");
+ closeChannel(clientChannel);
+ LOG.info("server channel status: " + (serverChannel.isOpen() ? "open" : "closed"));
+ LOG.info("closing server channel");
+ closeChannel(serverChannel);
+ }
+
+ private void closeChannel(Channel channel)
+ {
+ try
+ {
+ if (!channel.close().await(5L, TimeUnit.SECONDS))
+ {
+ LOG.warning("Failed to close connection within reasonable amount of time");
+ }
+ }
+ catch (InterruptedException e)
+ {
+ LOG.severe("Interrupted while closing connection");
+ }
+
+ }
+
+ private SocketChannel createClientChannel()
+ {
+ InetSocketAddress serverAddress = new InetSocketAddress("localhost", SERVER_PORT);
+ ChannelFuture clientChannelFuture = clientBootstrap.connect(serverAddress);
+ try
+ {
+ if (!clientChannelFuture.await(1000, TimeUnit.MILLISECONDS))
+ {
+ LOG.severe("did not connect within acceptable time period");
+ return null;
+ }
+ }
+ catch (InterruptedException e)
+ {
+ LOG.severe("Interrupted while waiting for client connect to be established");
+ return null;
+ }
+
+ if (!clientChannelFuture.isSuccess())
+ {
+ LOG.log(Level.SEVERE, "did not connect successfully", clientChannelFuture.getCause());
+ return null;
+ }
+
+ HttpTunnelClientChannelConfig config = ((HttpTunnelClientChannelConfig)clientChannelFuture.getChannel().getConfig());
+ config.setWriteBufferHighWaterMark(2 * 1024 * 1024);
+ config.setWriteBufferLowWaterMark(1024 * 1024);
+
+
+ return (SocketChannel) clientChannelFuture.getChannel();
+ }
+
+ private ChannelBuffer createRandomSizeBuffer(AtomicInteger nextWriteByte)
+ {
+ Random random = new Random();
+ int arraySize = random.nextInt(MAX_WRITE_SIZE) + 1;
+
+ // cheaply create the buffer by wrapping an appropriately sized section of the pre-built array
+ ChannelBuffer buffer = ChannelBuffers.wrappedBuffer(SEND_STREAM, nextWriteByte.get(), arraySize);
+ nextWriteByte.set((nextWriteByte.get() + arraySize) % 127);
+
+ return buffer;
+ }
+
+ public static void main(String[] args) throws Exception
+ {
+ HttpTunnelSoakTester soakTester = new HttpTunnelSoakTester();
+ try
+ {
+ soakTester.run();
+ }
+ finally
+ {
+ soakTester.shutdown();
+ }
+ }
+
+ private void shutdown()
+ {
+ serverBootstrap.releaseExternalResources();
+ clientBootstrap.releaseExternalResources();
+ executor.shutdownNow();
+ scheduledExecutor.shutdownNow();
+ }
+
+ private class DataVerifier extends SimpleChannelUpstreamHandler
+ {
+ private String name;
+
+ private int expectedNext = 0;
+
+ private int verifiedBytes = 0;
+
+ private CountDownLatch completionLatch = new CountDownLatch(1);
+
+ public DataVerifier(String name)
+ {
+ this.name = name;
+ }
+
+ @Override
+ public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception
+ {
+ ChannelBuffer bytesToVerify = (ChannelBuffer)e.getMessage();
+
+ while (bytesToVerify.readable())
+ {
+ byte readByte = bytesToVerify.readByte();
+ if (readByte != expectedNext)
+ {
+ LOG.log(Level.SEVERE, "{0}: received a byte out of sequence. Expected {1}, got {2}", new Object[] { name, expectedNext, readByte });
+ System.exit(-1);
+ return;
+ }
+
+ expectedNext = (expectedNext + 1) % 127;
+ verifiedBytes++;
+ }
+
+ if (verifiedBytes >= BYTES_TO_SEND)
+ {
+ completionLatch.countDown();
+ return;
+ }
+ }
+
+ @Override
+ public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ channels.add(ctx.getChannel());
+ }
+
+ public boolean waitForCompletion(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
+ return completionLatch.await(timeout, timeoutUnit);
+ }
+ }
+
+ private class SendThrottle extends SimpleChannelUpstreamHandler
+ {
+ private final DataSender sender;
+
+ public SendThrottle(DataSender sender)
+ {
+ this.sender = sender;
+ }
+
+ @Override
+ public void channelInterestChanged(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ boolean writeEnabled = ctx.getChannel().isWritable();
+ sender.setWriteEnabled(writeEnabled);
+
+ }
+ }
+
+ private class DataSender implements Runnable {
+
+ private AtomicReference channel = new AtomicReference();
+ private long totalBytesSent = 0;
+ private long numWrites = 0;
+ private long runStartTime = System.currentTimeMillis();
+ private boolean firstRun = true;
+ private AtomicBoolean writeEnabled = new AtomicBoolean(true);
+ private AtomicBoolean running = new AtomicBoolean(false);
+ private CountDownLatch finishLatch = new CountDownLatch(1);
+ private String name;
+ private AtomicInteger nextWriteByte = new AtomicInteger(0);
+
+ public DataSender(String name) {
+ this.name = name;
+ }
+
+ public void setChannel(Channel channel)
+ {
+ this.channel.set(channel);
+ }
+
+ public void setWriteEnabled(boolean enabled)
+ {
+ writeEnabled.set(enabled);
+ if(enabled && !this.isRunning() && finishLatch.getCount() > 0) {
+ executor.execute(this);
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ if(!running.compareAndSet(false, true)) {
+ LOG.log(Level.WARNING, "{0}: Attempt made to run duplicate sender!", name);
+ return;
+ }
+
+ if(finishLatch.getCount() == 0) {
+ LOG.log(Level.SEVERE, "{0}: Attempt made to run after completion!", name);
+ }
+
+ if(firstRun) {
+ firstRun = false;
+ runStartTime = System.currentTimeMillis();
+ LOG.log(Level.INFO, "{0}: sending data", name);
+ }
+
+ while (totalBytesSent < BYTES_TO_SEND)
+ {
+ if(!writeEnabled.get()) {
+ running.set(false);
+ return;
+ }
+
+ ChannelBuffer randomBytesForSend = createRandomSizeBuffer(nextWriteByte);
+ totalBytesSent += randomBytesForSend.readableBytes();
+
+ channel.get().write(ChannelBuffers.wrappedBuffer(randomBytesForSend));
+
+ numWrites++;
+ if (numWrites % 100 == 0)
+ {
+ LOG.log(Level.INFO, "{0}: {1} writes dispatched, totalling {2} bytes", new Object[]
+ {name, numWrites, totalBytesSent});
+ }
+ }
+
+ LOG.log(Level.INFO, "{0}: completed send cycle", name);
+
+ long runEndTime = System.currentTimeMillis();
+ long totalTime = runEndTime - runStartTime;
+ long totalKB = totalBytesSent / 1024;
+ double rate = totalKB / (totalTime / 1000.0);
+ LOG.log(Level.INFO, "{0}: Sent {1} bytes", new Object[] { name, totalBytesSent } );
+ LOG.log(Level.INFO, "{0}: Average throughput: {1} KB/s", new Object[] { name, rate } );
+
+ finishLatch.countDown();
+ running.set(false);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public boolean waitForFinish(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
+ return finishLatch.await(timeout, timeoutUnit);
+ }
+
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelTest.java
new file mode 100644
index 0000000000..6209b1eb9b
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/HttpTunnelTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.jboss.netty.bootstrap.ClientBootstrap;
+import org.jboss.netty.bootstrap.ServerBootstrap;
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelHandler;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
+import org.jboss.netty.channel.group.ChannelGroup;
+import org.jboss.netty.channel.group.DefaultChannelGroup;
+import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
+import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class HttpTunnelTest
+{
+
+ private HttpTunnelClientChannelFactory clientFactory;
+
+ private HttpTunnelServerChannelFactory serverFactory;
+
+ private ClientBootstrap clientBootstrap;
+
+ private ServerBootstrap serverBootstrap;
+
+ ChannelGroup activeConnections;
+
+ ChannelHandler clientCaptureHandler;
+
+ ServerEndHandler connectionCaptureHandler;
+
+ Channel serverEnd;
+
+ CountDownLatch serverEndLatch;
+
+ ChannelBuffer receivedBytes;
+
+ CountDownLatch messageReceivedLatch;
+
+ ChannelBuffer clientReceivedBytes;
+
+ CountDownLatch clientMessageReceivedLatch;
+
+ private Channel serverChannel;
+
+ @Before
+ public void setUp() throws UnknownHostException
+ {
+ activeConnections = new DefaultChannelGroup();
+ clientFactory = new HttpTunnelClientChannelFactory(new NioClientSocketChannelFactory(
+ Executors.newCachedThreadPool(), Executors.newCachedThreadPool()));
+ serverFactory = new HttpTunnelServerChannelFactory(new NioServerSocketChannelFactory(
+ Executors.newCachedThreadPool(), Executors.newCachedThreadPool()));
+
+ clientBootstrap = new ClientBootstrap(clientFactory);
+
+ clientCaptureHandler = new ClientEndHandler();
+ clientBootstrap.setPipelineFactory(new ChannelPipelineFactory()
+ {
+
+ public ChannelPipeline getPipeline() throws Exception
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("clientCapture", clientCaptureHandler);
+ return pipeline;
+ }
+ });
+
+ clientReceivedBytes = ChannelBuffers.dynamicBuffer();
+ clientMessageReceivedLatch = new CountDownLatch(1);
+
+ serverBootstrap = new ServerBootstrap(serverFactory);
+
+ connectionCaptureHandler = new ServerEndHandler();
+ serverBootstrap.setPipelineFactory(new ChannelPipelineFactory()
+ {
+
+ public ChannelPipeline getPipeline() throws Exception
+ {
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast("capture", connectionCaptureHandler);
+ return pipeline;
+ }
+ });
+
+ serverEndLatch = new CountDownLatch(1);
+ receivedBytes = ChannelBuffers.dynamicBuffer();
+ messageReceivedLatch = new CountDownLatch(1);
+
+ serverChannel = serverBootstrap.bind(new InetSocketAddress(InetAddress.getLocalHost(), 12345));
+ activeConnections.add(serverChannel);
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ activeConnections.disconnect().await(1000L);
+ clientBootstrap.releaseExternalResources();
+ serverBootstrap.releaseExternalResources();
+ }
+
+ @Test(timeout = 2000)
+ public void testConnectClientToServer() throws Exception
+ {
+ ChannelFuture connectFuture = clientBootstrap.connect(new InetSocketAddress(InetAddress.getLocalHost(), 12345));
+ assertTrue(connectFuture.await(1000L));
+ assertTrue(connectFuture.isSuccess());
+ assertNotNull(connectFuture.getChannel());
+
+ Channel clientChannel = connectFuture.getChannel();
+ activeConnections.add(clientChannel);
+ assertEquals(serverChannel.getLocalAddress(), clientChannel.getRemoteAddress());
+
+ assertTrue(serverEndLatch.await(1000, TimeUnit.MILLISECONDS));
+ assertNotNull(serverEnd);
+ assertEquals(clientChannel.getLocalAddress(), serverEnd.getRemoteAddress());
+ }
+
+ @Test
+ public void testSendDataFromClientToServer() throws Exception
+ {
+ ChannelFuture connectFuture = clientBootstrap.connect(new InetSocketAddress(InetAddress.getLocalHost(), 12345));
+ assertTrue(connectFuture.await(1000L));
+
+ Channel clientEnd = connectFuture.getChannel();
+ activeConnections.add(clientEnd);
+
+ assertTrue(serverEndLatch.await(1000, TimeUnit.MILLISECONDS));
+
+ ChannelFuture writeFuture = Channels.write(clientEnd, NettyTestUtils.createData(100L));
+ assertTrue(writeFuture.await(1000L));
+ assertTrue(writeFuture.isSuccess());
+
+ assertTrue(messageReceivedLatch.await(1000L, TimeUnit.MILLISECONDS));
+ assertEquals(100L, receivedBytes.readLong());
+ }
+
+ @Test
+ public void testSendDataFromServerToClient() throws Exception
+ {
+ ChannelFuture connectFuture = clientBootstrap.connect(new InetSocketAddress(InetAddress.getLocalHost(), 12345));
+ assertTrue(connectFuture.await(1000L));
+
+ Channel clientEnd = connectFuture.getChannel();
+ activeConnections.add(clientEnd);
+
+ assertTrue(serverEndLatch.await(1000, TimeUnit.MILLISECONDS));
+
+ ChannelFuture writeFuture = Channels.write(serverEnd, NettyTestUtils.createData(4321L));
+ assertTrue(writeFuture.await(1000L));
+ assertTrue(writeFuture.isSuccess());
+
+ assertTrue(clientMessageReceivedLatch.await(1000, TimeUnit.MILLISECONDS));
+ assertEquals(4321L, clientReceivedBytes.readLong());
+ }
+
+ class ServerEndHandler extends SimpleChannelUpstreamHandler
+ {
+
+ @Override
+ public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception
+ {
+ serverEnd = e.getChannel();
+ activeConnections.add(serverEnd);
+ serverEndLatch.countDown();
+ super.channelConnected(ctx, e);
+ }
+
+ @Override
+ public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception
+ {
+ receivedBytes.writeBytes((ChannelBuffer) e.getMessage());
+ messageReceivedLatch.countDown();
+ }
+ }
+
+ class ClientEndHandler extends SimpleChannelUpstreamHandler
+ {
+
+ @Override
+ public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception
+ {
+ clientReceivedBytes.writeBytes((ChannelBuffer) e.getMessage());
+ clientMessageReceivedLatch.countDown();
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/MockChannelStateListener.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/MockChannelStateListener.java
new file mode 100644
index 0000000000..fd5cb10ec3
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/MockChannelStateListener.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.ChannelFuture;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class MockChannelStateListener implements HttpTunnelClientWorkerOwner
+{
+
+ public boolean fullyEstablished = false;
+
+ public List messages = new ArrayList();
+
+ public String tunnelId = null;
+
+ public String serverHostName = null;
+
+ public void fullyEstablished()
+ {
+ fullyEstablished = true;
+ }
+
+ public void onConnectRequest(ChannelFuture connectFuture, InetSocketAddress remoteAddress)
+ {
+ // not relevant for test
+ }
+
+ public void onMessageReceived(ChannelBuffer content)
+ {
+ messages.add(content);
+ }
+
+ public void onTunnelOpened(String tunnelId)
+ {
+ this.tunnelId = tunnelId;
+ }
+
+ public String getServerHostName()
+ {
+ return serverHostName;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtils.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtils.java
new file mode 100644
index 0000000000..1358aa9bdb
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtils.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import junit.framework.Assert;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelState;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.DownstreamMessageEvent;
+import org.jboss.netty.channel.ExceptionEvent;
+import org.jboss.netty.channel.UpstreamMessageEvent;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class NettyTestUtils
+{
+
+ public static ByteBuffer convertReadable(ChannelBuffer b)
+ {
+ int startIndex = b.readerIndex();
+ ByteBuffer converted = ByteBuffer.allocate(b.readableBytes());
+ b.readBytes(converted);
+ b.readerIndex(startIndex);
+ converted.flip();
+ return converted;
+ }
+
+ public static void assertEquals(ChannelBuffer expected, ChannelBuffer actual)
+ {
+ if (expected.readableBytes() != actual.readableBytes())
+ {
+ Assert.failNotEquals("channel buffers have differing readable sizes", expected.readableBytes(),
+ actual.readableBytes());
+ }
+
+ int startPositionExpected = expected.readerIndex();
+ int startPositionActual = actual.readerIndex();
+ int position = 0;
+ while (expected.readable())
+ {
+ byte expectedByte = expected.readByte();
+ byte actualByte = actual.readByte();
+ if (expectedByte != actualByte)
+ {
+ Assert.failNotEquals("channel buffers differ at position " + position, expectedByte, actualByte);
+ }
+
+ position++;
+ }
+
+ expected.readerIndex(startPositionExpected);
+ actual.readerIndex(startPositionActual);
+ }
+
+ public static boolean checkEquals(ChannelBuffer expected, ChannelBuffer actual)
+ {
+ if (expected.readableBytes() != actual.readableBytes())
+ {
+ return false;
+ }
+
+ int position = 0;
+ while (expected.readable())
+ {
+ byte expectedByte = expected.readByte();
+ byte actualByte = actual.readByte();
+ if (expectedByte != actualByte)
+ {
+ return false;
+ }
+
+ position++;
+ }
+
+ return true;
+ }
+
+ public static List splitIntoChunks(int chunkSize, ChannelBuffer... buffers)
+ {
+ LinkedList chunks = new LinkedList();
+
+ ArrayList sourceBuffers = new ArrayList();
+ Collections.addAll(sourceBuffers, buffers);
+ Iterator sourceIter = sourceBuffers.iterator();
+ ChannelBuffer chunk = ChannelBuffers.buffer(chunkSize);
+ while (sourceIter.hasNext())
+ {
+ ChannelBuffer source = sourceIter.next();
+
+ int index = source.readerIndex();
+ while (source.writerIndex() > index)
+ {
+ int fragmentSize = Math.min(source.writerIndex() - index, chunk.writableBytes());
+ chunk.writeBytes(source, index, fragmentSize);
+ if (!chunk.writable())
+ {
+ chunks.add(chunk);
+ chunk = ChannelBuffers.buffer(chunkSize);
+ }
+ index += fragmentSize;
+ }
+ }
+
+ if (chunk.readable())
+ {
+ chunks.add(chunk);
+ }
+
+ return chunks;
+ }
+
+ public static ChannelBuffer createData(long containedNumber)
+ {
+ ChannelBuffer data = ChannelBuffers.dynamicBuffer();
+ data.writeLong(containedNumber);
+ return data;
+ }
+
+ public static void checkIsUpstreamMessageEventContainingData(ChannelEvent event, ChannelBuffer expectedData)
+ {
+ ChannelBuffer data = checkIsUpstreamMessageEvent(event, ChannelBuffer.class);
+ assertEquals(expectedData, data);
+ }
+
+ public static T checkIsUpstreamMessageEvent(ChannelEvent event, Class expectedMessageType)
+ {
+ assertTrue(event instanceof UpstreamMessageEvent);
+ UpstreamMessageEvent messageEvent = (UpstreamMessageEvent) event;
+ assertTrue(expectedMessageType.isInstance(messageEvent.getMessage()));
+ return expectedMessageType.cast(messageEvent.getMessage());
+ }
+
+ public static T checkIsDownstreamMessageEvent(ChannelEvent event, Class expectedMessageType)
+ {
+ assertTrue(event instanceof DownstreamMessageEvent);
+ DownstreamMessageEvent messageEvent = (DownstreamMessageEvent) event;
+ assertTrue(expectedMessageType.isInstance(messageEvent.getMessage()));
+ return expectedMessageType.cast(messageEvent.getMessage());
+ }
+
+ public static InetSocketAddress createAddress(byte[] addr, int port)
+ {
+ try
+ {
+ return new InetSocketAddress(InetAddress.getByAddress(addr), port);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new RuntimeException("Bad address in test");
+ }
+ }
+
+ public static Throwable checkIsExceptionEvent(ChannelEvent ev)
+ {
+ assertTrue(ev instanceof ExceptionEvent);
+ ExceptionEvent exceptionEv = (ExceptionEvent) ev;
+ return exceptionEv.getCause();
+ }
+
+ public static ChannelStateEvent checkIsStateEvent(ChannelEvent event, ChannelState expectedState,
+ Object expectedValue)
+ {
+ assertTrue(event instanceof ChannelStateEvent);
+ ChannelStateEvent stateEvent = (ChannelStateEvent) event;
+ Assert.assertEquals(expectedState, stateEvent.getState());
+ Assert.assertEquals(expectedValue, stateEvent.getValue());
+ return stateEvent;
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtilsTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtilsTest.java
new file mode 100644
index 0000000000..2b9987993d
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NettyTestUtilsTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class NettyTestUtilsTest
+{
+
+ @Test
+ public void testSplitIntoChunks()
+ {
+ ChannelBuffer a = createFullBuffer(20, (byte) 0);
+ ChannelBuffer b = createFullBuffer(20, (byte) 1);
+ ChannelBuffer c = createFullBuffer(20, (byte) 2);
+
+ List chunks = NettyTestUtils.splitIntoChunks(10, a, b, c);
+ assertEquals(6, chunks.size());
+ for (ChannelBuffer chunk : chunks)
+ {
+ assertEquals(10, chunk.readableBytes());
+ }
+
+ // reader index should not be modified by splitIntoChunks()
+ assertEquals(0, a.readerIndex());
+ assertEquals(0, b.readerIndex());
+ assertEquals(0, c.readerIndex());
+ }
+
+ @Test
+ public void testSplitIntoChunks_chunksCrossBoundaries()
+ {
+ ChannelBuffer a = createFullBuffer(5, (byte) 0);
+ ChannelBuffer b = createFullBuffer(5, (byte) 1);
+ ChannelBuffer c = createFullBuffer(5, (byte) 2);
+
+ List chunks = NettyTestUtils.splitIntoChunks(4, a, b, c);
+ assertEquals(4, chunks.size());
+ checkBufferContains(chunks.get(0), new byte[]
+ {0, 0, 0, 0});
+ checkBufferContains(chunks.get(1), new byte[]
+ {0, 1, 1, 1});
+ checkBufferContains(chunks.get(2), new byte[]
+ {1, 1, 2, 2});
+ checkBufferContains(chunks.get(3), new byte[]
+ {2, 2, 2});
+ }
+
+ @Test
+ public void testSplitIntoChunks_smallestChunksPossible()
+ {
+ ChannelBuffer a = createFullBuffer(5, (byte) 0);
+ ChannelBuffer b = createFullBuffer(5, (byte) 1);
+ ChannelBuffer c = createFullBuffer(5, (byte) 2);
+
+ List chunks = NettyTestUtils.splitIntoChunks(1, a, b, c);
+ assertEquals(15, chunks.size());
+ checkBufferContains(chunks.get(0), new byte[]
+ {0});
+ checkBufferContains(chunks.get(5), new byte[]
+ {1});
+ checkBufferContains(chunks.get(10), new byte[]
+ {2});
+ }
+
+ @Test
+ public void testSplitIntoChunks_sourceBuffersArePartiallyRead()
+ {
+ ChannelBuffer a = createFullBuffer(5, (byte) 0);
+ a.readerIndex(1);
+ ChannelBuffer b = createFullBuffer(5, (byte) 1);
+ b.readerIndex(2);
+ ChannelBuffer c = createFullBuffer(5, (byte) 2);
+
+ // will be ignored, as fully read
+ ChannelBuffer d = createFullBuffer(5, (byte) 3);
+ d.readerIndex(5);
+ ChannelBuffer e = createFullBuffer(5, (byte) 4);
+ e.readerIndex(4);
+
+ List chunks = NettyTestUtils.splitIntoChunks(3, a, b, c, d, e);
+ checkBufferContains(chunks.get(0), new byte[]
+ {0, 0, 0});
+ checkBufferContains(chunks.get(1), new byte[]
+ {0, 1, 1});
+ checkBufferContains(chunks.get(2), new byte[]
+ {1, 2, 2});
+ checkBufferContains(chunks.get(3), new byte[]
+ {2, 2, 2});
+ checkBufferContains(chunks.get(4), new byte[]
+ {4});
+ }
+
+ private void checkBufferContains(ChannelBuffer channelBuffer, byte[] bs)
+ {
+ if (channelBuffer.readableBytes() != bs.length)
+ {
+ fail("buffer does not have enough bytes");
+ }
+
+ for (int i = 0; i < bs.length; i++)
+ {
+ assertEquals("byte at position " + i + " does not match", bs[i], channelBuffer.getByte(i));
+ }
+ }
+
+ private ChannelBuffer createFullBuffer(int size, byte value)
+ {
+ ChannelBuffer buffer = ChannelBuffers.buffer(size);
+ byte[] contents = new byte[size];
+ for (int i = 0; i < contents.length; i++)
+ {
+ contents[i] = value;
+ }
+ buffer.writeBytes(contents);
+ return buffer;
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/NullChannelHandler.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NullChannelHandler.java
new file mode 100644
index 0000000000..1ece636a9f
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/NullChannelHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import org.jboss.netty.channel.ChannelDownstreamHandler;
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelUpstreamHandler;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class NullChannelHandler implements ChannelUpstreamHandler, ChannelDownstreamHandler
+{
+
+ public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception
+ {
+ ctx.sendUpstream(e);
+ }
+
+ public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception
+ {
+ ctx.sendDownstream(e);
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/SaturationManagerTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/SaturationManagerTest.java
new file mode 100644
index 0000000000..d60340d92f
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/SaturationManagerTest.java
@@ -0,0 +1,34 @@
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.*;
+import static org.jboss.netty.channel.socket.httptunnel.SaturationStateChange.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class SaturationManagerTest
+{
+
+ private SaturationManager manager;
+
+ @Before
+ public void setUp() {
+ manager = new SaturationManager(100L, 200L);
+ }
+
+ @Test
+ public void testQueueSizeChanged()
+ {
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(100L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(99L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(1L));
+ assertEquals(SATURATED, manager.queueSizeChanged(1L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(10L));
+
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-10L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-1L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-1L));
+ assertEquals(DESATURATED, manager.queueSizeChanged(-99L));
+ assertEquals(NO_CHANGE, manager.queueSizeChanged(-100L));
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchTest.java
new file mode 100644
index 0000000000..8851804ffc
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/ServerMessageSwitchTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.InetSocketAddress;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.httptunnel.ServerMessageSwitchUpstreamInterface.TunnelStatus;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+import org.jmock.Expectations;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+@RunWith(JMock.class)
+public class ServerMessageSwitchTest
+{
+
+ public static final InetSocketAddress REMOTE_ADDRESS = InetSocketAddress.createUnresolved("test.client.com", 52354);
+
+ private final JUnit4Mockery mockContext = new JUnit4Mockery();
+
+ private ServerMessageSwitch messageSwitch;
+
+ HttpTunnelAcceptedChannelFactory newChannelFactory;
+
+ private FakeChannelSink responseCatcher;
+
+ private FakeSocketChannel htunChannel;
+
+ private FakeSocketChannel requesterChannel;
+
+ private HttpTunnelAcceptedChannelReceiver htunAcceptedChannel;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ newChannelFactory = mockContext.mock(HttpTunnelAcceptedChannelFactory.class);
+ messageSwitch = new ServerMessageSwitch(newChannelFactory);
+
+ htunAcceptedChannel = mockContext.mock(HttpTunnelAcceptedChannelReceiver.class);
+ createRequesterChannel();
+
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(newChannelFactory).newChannel(with(any(String.class)), with(equal(REMOTE_ADDRESS)));
+ will(returnValue(htunAcceptedChannel));
+ ignoring(newChannelFactory).generateTunnelId();
+ will(returnValue("TEST_TUNNEL"));
+ }
+ });
+ }
+
+ private FakeSocketChannel createRequesterChannel()
+ {
+ ChannelPipeline requesterChannelPipeline = Channels.pipeline();
+ responseCatcher = new FakeChannelSink();
+ requesterChannel = new FakeSocketChannel(null, null, requesterChannelPipeline, responseCatcher);
+ responseCatcher.events.clear();
+
+ return requesterChannel;
+ }
+
+ @Test
+ public void testRouteInboundData()
+ {
+ final ChannelBuffer inboundData = ChannelBuffers.dynamicBuffer();
+ inboundData.writeLong(1234L);
+
+ mockContext.checking(new Expectations()
+ {
+ {
+ one(htunAcceptedChannel).dataReceived(with(same(inboundData)));
+ }
+ });
+
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.routeInboundData(tunnelId, inboundData);
+ mockContext.assertIsSatisfied();
+ }
+
+ @Test
+ public void testRouteOutboundData_onPoll()
+ {
+ ChannelBuffer outboundData = ChannelBuffers.dynamicBuffer();
+ outboundData.writeLong(1234L);
+
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.routeOutboundData(tunnelId, outboundData, Channels.future(htunChannel));
+ messageSwitch.pollOutboundData(tunnelId, requesterChannel);
+
+ assertEquals(1, responseCatcher.events.size());
+ HttpResponse response = NettyTestUtils.checkIsDownstreamMessageEvent(responseCatcher.events.poll(),
+ HttpResponse.class);
+ NettyTestUtils.assertEquals(outboundData, response.getContent());
+ }
+
+ @Test
+ public void testRouteOutboundData_withDanglingRequest()
+ {
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.pollOutboundData(tunnelId, requesterChannel);
+ assertEquals(0, responseCatcher.events.size());
+
+ ChannelBuffer outboundData = ChannelBuffers.dynamicBuffer();
+ outboundData.writeLong(1234L);
+
+ messageSwitch.routeOutboundData(tunnelId, outboundData, Channels.future(htunChannel));
+ assertEquals(1, responseCatcher.events.size());
+ HttpResponse response = NettyTestUtils.checkIsDownstreamMessageEvent(responseCatcher.events.poll(),
+ HttpResponse.class);
+ NettyTestUtils.assertEquals(outboundData, response.getContent());
+ }
+
+ @Test
+ public void testCloseTunnel()
+ {
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.serverCloseTunnel(tunnelId);
+ assertEquals(TunnelStatus.CLOSED, messageSwitch.routeInboundData(tunnelId, ChannelBuffers.dynamicBuffer()));
+ }
+
+ /* TODO: require tests that check the various permutations of a client sending or polling
+ data after the server has closed the connection */
+
+ /* TODO: require tests that check what happens when a client closes a connection */
+
+ @Test
+ public void testRouteInboundDataIgnoredAfterClose()
+ {
+ ChannelBuffer data = NettyTestUtils.createData(1234L);
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.serverCloseTunnel(tunnelId);
+
+ mockContext.checking(new Expectations()
+ {
+ {
+ never(htunAcceptedChannel).dataReceived(with(any(ChannelBuffer.class)));
+ }
+ });
+
+ messageSwitch.routeInboundData(tunnelId, data);
+ mockContext.assertIsSatisfied();
+ }
+
+ @Test
+ public void testRouteOutboundDataIgnoredAfterClose()
+ {
+ ChannelBuffer data = NettyTestUtils.createData(1234L);
+ String tunnelId = messageSwitch.createTunnel(REMOTE_ADDRESS);
+ messageSwitch.serverCloseTunnel(tunnelId);
+ messageSwitch.routeOutboundData(tunnelId, data, Channels.future(htunChannel));
+ assertEquals(0, responseCatcher.events.size());
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/UpstreamEventCatcher.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/UpstreamEventCatcher.java
new file mode 100644
index 0000000000..91fa22b833
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/UpstreamEventCatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.jboss.netty.channel.ChannelEvent;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelUpstreamHandler;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class UpstreamEventCatcher implements ChannelUpstreamHandler
+{
+
+ public static final String NAME = "upstreamCatcher";
+
+ public Queue events = new LinkedList();
+
+ public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception
+ {
+ events.add(e);
+ }
+
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenterTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenterTest.java
new file mode 100644
index 0000000000..3aa294f445
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteFragmenterTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelFuture;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.MessageEvent;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class WriteFragmenterTest
+{
+
+ private FakeSocketChannel channel;
+
+ private WriteFragmenter fragmenter;
+
+ private FakeChannelSink downstreamCatcher;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ fragmenter = new WriteFragmenter(100);
+
+ ChannelPipeline pipeline = Channels.pipeline();
+ pipeline.addLast(WriteFragmenter.NAME, fragmenter);
+ downstreamCatcher = new FakeChannelSink();
+ channel = new FakeSocketChannel(null, null, pipeline, downstreamCatcher);
+ }
+
+ @Test
+ public void testLeavesWritesBeneathThresholdUntouched()
+ {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[99]);
+ Channels.write(channel, data);
+
+ assertEquals(1, downstreamCatcher.events.size());
+ ChannelBuffer sentData = NettyTestUtils.checkIsDownstreamMessageEvent(downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ assertSame(data, sentData);
+ }
+
+ @Test
+ public void testLeavesMessagesOnThresholdUntouched()
+ {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[100]);
+ Channels.write(channel, data);
+
+ assertEquals(1, downstreamCatcher.events.size());
+ ChannelBuffer sentData = NettyTestUtils.checkIsDownstreamMessageEvent(downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ assertSame(data, sentData);
+ }
+
+ @Test
+ public void testSplitsMessagesAboveThreshold_twoChunks()
+ {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[101]);
+ Channels.write(channel, data);
+
+ assertEquals(2, downstreamCatcher.events.size());
+ ChannelBuffer chunk1 = NettyTestUtils.checkIsDownstreamMessageEvent(downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ ChannelBuffer chunk2 = NettyTestUtils.checkIsDownstreamMessageEvent(downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ assertEquals(100, chunk1.readableBytes());
+ assertEquals(1, chunk2.readableBytes());
+ }
+
+ @Test
+ public void testSplitsMessagesAboveThreshold_multipleChunks()
+ {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[2540]);
+ Channels.write(channel, data);
+
+ assertEquals(26, downstreamCatcher.events.size());
+ for (int i = 0; i < 25; i++)
+ {
+ ChannelBuffer chunk = NettyTestUtils.checkIsDownstreamMessageEvent(downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ assertEquals(100, chunk.readableBytes());
+ }
+
+ ChannelBuffer endChunk = NettyTestUtils.checkIsDownstreamMessageEvent(downstreamCatcher.events.poll(),
+ ChannelBuffer.class);
+ assertEquals(40, endChunk.readableBytes());
+ }
+
+ @Test
+ public void testChannelFutureTriggeredOnlyWhenAllChunksWritten()
+ {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[2540]);
+ ChannelFuture mainWriteFuture = Channels.write(channel, data);
+
+ assertEquals(26, downstreamCatcher.events.size());
+ for (int i = 0; i < 25; i++)
+ {
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture().setSuccess();
+ assertFalse(mainWriteFuture.isDone());
+ }
+
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture().setSuccess();
+ assertTrue(mainWriteFuture.isDone());
+ assertTrue(mainWriteFuture.isSuccess());
+ }
+
+ @Test
+ public void testChannelFutureFailsOnFirstWriteFailure()
+ {
+ ChannelBuffer data = ChannelBuffers.wrappedBuffer(new byte[2540]);
+ ChannelFuture mainWriteFuture = Channels.write(channel, data);
+
+ assertEquals(26, downstreamCatcher.events.size());
+ for (int i = 0; i < 10; i++)
+ {
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture().setSuccess();
+ assertFalse(mainWriteFuture.isDone());
+ }
+
+ ((MessageEvent) downstreamCatcher.events.poll()).getFuture().setFailure(new Exception("Something bad happened"));
+ assertTrue(mainWriteFuture.isDone());
+ assertFalse(mainWriteFuture.isSuccess());
+
+ // check all the subsequent writes got cancelled
+ for (int i = 0; i < 15; i++)
+ {
+ assertTrue(((MessageEvent) downstreamCatcher.events.poll()).getFuture().isCancelled());
+ }
+ }
+}
diff --git a/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitterTest.java b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitterTest.java
new file mode 100644
index 0000000000..c4453b14e0
--- /dev/null
+++ b/src/test/java/org/jboss/netty/channel/socket/httptunnel/WriteSplitterTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2009 Red Hat, Inc.
+ *
+ * Red Hat 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.
+ */
+
+package org.jboss.netty.channel.socket.httptunnel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.junit.Test;
+
+/**
+ * @author The Netty Project (netty-dev@lists.jboss.org)
+ * @author Iain McGinniss (iain.mcginniss@onedrum.com)
+ */
+public class WriteSplitterTest
+{
+
+ private static final int SPLIT_THRESHOLD = 1024;
+
+ @Test
+ public void testSplit_bufferUnderThreshold()
+ {
+ ChannelBuffer buffer = createBufferWithContents(800);
+ List fragments = WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(1, fragments.size());
+ }
+
+ @Test
+ public void testSplit_bufferMatchesThreshold()
+ {
+ ChannelBuffer buffer = createBufferWithContents(SPLIT_THRESHOLD);
+ List fragments = WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(1, fragments.size());
+ }
+
+ @Test
+ public void testSplit_bufferOverThreshold()
+ {
+ ChannelBuffer buffer = createBufferWithContents((int) (SPLIT_THRESHOLD * 1.5));
+ List fragments = WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(2, fragments.size());
+
+ ChannelBuffer fragment1 = fragments.get(0);
+ checkMatches(buffer, fragment1);
+ ChannelBuffer fragment2 = fragments.get(1);
+ checkMatches(buffer, fragment2);
+ }
+
+ @Test
+ public void testSplit_largeNumberOfFragments()
+ {
+ ChannelBuffer buffer = createBufferWithContents(SPLIT_THRESHOLD * 250);
+ List fragments = WriteSplitter.split(buffer, SPLIT_THRESHOLD);
+ assertNotNull(fragments);
+ assertEquals(250, fragments.size());
+
+ for (ChannelBuffer fragment : fragments)
+ {
+ checkMatches(buffer, fragment);
+ }
+ }
+
+ private void checkMatches(ChannelBuffer mainBuffer, ChannelBuffer fragment)
+ {
+ assertTrue(mainBuffer.readableBytes() >= fragment.readableBytes());
+ while (fragment.readable())
+ {
+ assertEquals(mainBuffer.readByte(), fragment.readByte());
+ }
+ }
+
+ private ChannelBuffer createBufferWithContents(int size)
+ {
+ byte[] contents = new byte[size];
+ for (int i = 0; i < contents.length; i++)
+ {
+ contents[i] = (byte) (i % 10);
+ }
+
+ return ChannelBuffers.copiedBuffer(contents);
+ }
+
+}