/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.protocol.http.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.logging.LogLevel;
import io.reactivex.netty.protocol.http.client.HttpClient;
import io.reactivex.netty.protocol.http.client.HttpClientResponse;
import org.junit.Assert;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func0;
import rx.functions.Func1;
import rx.observers.TestSubscriber;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.Charset;
import java.util.regex.Pattern;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
public class HttpServerRule extends ExternalResource {
public static final String WELCOME_SERVER_MSG = "Welcome!";
private HttpServer<ByteBuf, ByteBuf> server;
private HttpClient<ByteBuf, ByteBuf> client;
private String lastResponse = "";
@Override
public Statement apply(final Statement base, Description description) {
lastResponse = "";
return new Statement() {
@Override
public void evaluate() throws Throwable {
server = HttpServer.newServer()
.enableWireLogging("test", LogLevel.INFO)
.addChannelHandlerFirst("raw-message-handler",
RawMessageHandler.factory(
new Action1<ByteBuf>() {
@Override
public void call(ByteBuf byteBuf) {
lastResponse += byteBuf.toString(Charset.defaultCharset());
}
}
)
);
base.evaluate();
}
};
}
public SocketAddress startServer() {
server.start(new RequestHandler<ByteBuf, ByteBuf>() {
@Override
public Observable<Void> handle(HttpServerRequest<ByteBuf> request,
HttpServerResponse<ByteBuf> response) {
return response.setHeader(CONTENT_LENGTH, WELCOME_SERVER_MSG.getBytes().length)
.writeString(Observable.just(WELCOME_SERVER_MSG));
}
});
client = HttpClient.newClient("127.0.0.1", server.getServerPort());
return server.getServerAddress();
}
public void startServer(RequestHandler<ByteBuf, ByteBuf> handler) {
server.start(handler);
client = HttpClient.newClient("127.0.0.1", server.getServerPort());
}
public void setupClient(HttpClient<ByteBuf, ByteBuf> client) {
this.client = client;
}
public HttpClientResponse<ByteBuf> sendRequest(Observable<HttpClientResponse<ByteBuf>> request) {
TestSubscriber<HttpClientResponse<ByteBuf>> subscriber = new TestSubscriber<>();
request.subscribe(subscriber);
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
assertThat("Unexpected response count.", subscriber.getOnNextEvents(), hasSize(1));
return subscriber.getOnNextEvents().get(0);
}
public void assertResponseContent(HttpClientResponse<ByteBuf> response) {
TestSubscriber<ByteBuf> subscriber = new TestSubscriber<>();
response.getContent().subscribe(subscriber);
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
assertThat("Unexpected content items.", subscriber.getOnNextEvents(), hasSize(1));
assertThat("Unexpected content.", subscriber.getOnNextEvents().get(0).toString(Charset.defaultCharset()),
equalTo(WELCOME_SERVER_MSG));
}
public void assertEmptyBodyWithContentLengthZero() {
assertBodyWithContentLength(0, "");
}
public void assertBodyWithContentLength(int contentLength, String body) {
getAndDrainClient();
Pattern headerBlock = Pattern.compile("^(.*?\r\n)*?\r\n", Pattern.MULTILINE);
if (!lastResponse.contains("content-length: " + contentLength + "\r\n")) {
Assert.fail("Missing header 'content-length: " + contentLength + "'");
}
if (lastResponse.contains("transfer-encoding: chunked\r\n")) {
Assert.fail("Unexpected header 'transfer-encoding: chunked'");
}
if (!headerBlock.matcher(lastResponse).replaceFirst("").equals(body)) {
Assert.fail("Unexpected body content '" + headerBlock.matcher(lastResponse).replaceFirst("") + "'");
}
}
public void assertEmptyBodyWithSingleChunk() {
assertChunks();
}
public void assertChunks(String... chunks) {
getAndDrainClient();
Pattern headerBlock = Pattern.compile("^(.*?\r\n)*?\r\n", Pattern.MULTILINE);
if (lastResponse.contains("content-length: 0\r\n")) {
Assert.fail("Unexpected header 'content-length: 0'");
}
if (!lastResponse.contains("transfer-encoding: chunked\r\n")) {
Assert.fail("Missing header 'transfer-encoding: chunked'");
}
String expectedChunkContent = "";
for (String c : chunks) {
expectedChunkContent += c.getBytes().length + "\r\n";
expectedChunkContent += c + "\r\n";
}
expectedChunkContent += "0\r\n\r\n";
if (!headerBlock.matcher(lastResponse).replaceFirst("").equals(expectedChunkContent)) {
Assert.fail("Unexpected body content '" + headerBlock.matcher(lastResponse).replaceFirst("") + "'");
}
}
public SocketAddress getServerAddress() {
return new InetSocketAddress("127.0.0.1", server.getServerPort());
}
public void setServer(HttpServer<ByteBuf, ByteBuf> server) {
this.server = server;
}
public HttpServer<ByteBuf, ByteBuf> getServer() {
return server;
}
public HttpClient<ByteBuf, ByteBuf> getClient() {
return client;
}
public void getAndDrainClient() {
lastResponse = "";
TestSubscriber<Void> clientDrain = new TestSubscriber<>();
client.createGet("/")
.flatMap(new Func1<HttpClientResponse<ByteBuf>, Observable<Void>>() {
@Override
public Observable<Void> call(HttpClientResponse<ByteBuf> clientResponse) {
return clientResponse.discardContent();
}
})
.subscribe(clientDrain);
clientDrain.awaitTerminalEvent();
clientDrain.assertNoErrors();
}
private static class RawMessageHandler extends ChannelDuplexHandler {
public static Func0<ChannelHandler> factory(final Action1<ByteBuf> onWrite) {
return new Func0<ChannelHandler>() {
@Override
public ChannelHandler call() {
return new RawMessageHandler(onWrite);
}
};
}
private final Action1<ByteBuf> onWrite;
public RawMessageHandler(Action1<ByteBuf> onWrite) {
this.onWrite = onWrite;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
callback(msg, onWrite);
super.write(ctx, msg, promise);
}
private void callback(Object msg, Action1<ByteBuf> a) {
if (msg instanceof ByteBuf) {
a.call((ByteBuf) msg);
} else if (msg instanceof ByteBufHolder) {
a.call(((ByteBufHolder) msg).content());
} else {
throw new RuntimeException("Unexpected msg type " + msg.getClass());
}
}
}
}