/*
* Copyright 2012 Jason Miller
*
* 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 jj.http.server;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import java.io.IOException;
import jj.event.Publisher;
import jj.http.server.EngineHttpHandler;
import jj.http.server.HttpServerRequestImpl;
import jj.http.server.uri.Route;
import jj.http.server.uri.RouteMatch;
import jj.http.server.uri.Router;
import jj.http.server.uri.URIMatch;
import jj.http.server.websocket.WebSocketConnectionMaker;
import jj.http.server.websocket.WebSocketRequestChecker;
import jj.logging.Emergency;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.binder.AnnotatedBindingBuilder;
/**
* @author jason
*
*/
@RunWith(MockitoJUnitRunner.class)
public class EngineHttpHandlerTest {
@Mock Publisher publisher;
@Mock ChannelHandlerContext ctx;
@Mock Channel channel;
@Mock ChannelFuture channelFuture;
@Mock Injector injector;
@Mock Binder binder;
@Mock AnnotatedBindingBuilder<Object> abb;
@Captor ArgumentCaptor<Module> moduleCaptor;
@Captor ArgumentCaptor<FullHttpResponse> responseCaptor;
@Captor ArgumentCaptor<ChannelFutureListener> futureListenerCaptor;
@Mock WebSocketRequestChecker webSocketRequestChecker;
@Mock WebSocketConnectionMaker webSocketConnectionMaker;
@Mock HttpServerRequestImpl httpRequest1;
@Mock HttpServerRequestImpl httpRequest2;
@Mock HttpServerRequestImpl httpRequest3;
@Mock HttpServerRequestImpl httpRequest4;
@Mock HttpServerRequestImpl httpRequest5;
@Mock HttpServerResponse httpResponse;
EngineHttpHandler handler;
@Mock ServableResources servableResources;
@Mock Router router;
@Mock RouteMatch routeMatch;
@Mock Route route;
String resourceName = "resource";
@Mock RouteProcessor routeProcessor;
//given
@Before
public void before() throws Exception {
handler = new EngineHttpHandler(servableResources, router, injector, webSocketRequestChecker, publisher);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void prepareInjectorStubbing() {
// little ugly, but sets up an injector that will return our mocks
given(injector.getInstance(HttpServerRequest.class)).willReturn(httpRequest1);
given(injector.getInstance(HttpServerResponse.class)).willReturn(httpResponse);
given(injector.getInstance(WebSocketConnectionMaker.class)).willReturn(webSocketConnectionMaker);
given(injector.createChildInjector(any(Module.class))).willReturn(injector);
given(injector.createChildInjector(any(Module.class), any(Module.class))).willReturn(injector);
given(binder.bind((Class)any())).willReturn(abb);
}
@Test
public void testReceivedBadRequest() throws Exception {
FullHttpRequest fullHttpRequest = mock(FullHttpRequest.class, RETURNS_DEEP_STUBS);
given(fullHttpRequest.decoderResult().isSuccess()).willReturn(false);
prepareInjectorStubbing();
handler.channelRead0(ctx, fullHttpRequest);
verify(httpResponse).sendError(HttpResponseStatus.BAD_REQUEST);
}
@Test
public void testReceivedWebSocketRequest() throws Exception {
FullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
given(webSocketRequestChecker.isWebSocketRequest(fullHttpRequest)).willReturn(true);
prepareInjectorStubbing();
handler.channelRead0(ctx, fullHttpRequest);
verify(webSocketConnectionMaker).handshakeWebsocket();
}
private void givenRouting() {
given(routeMatch.route()).willReturn(route);
given(routeMatch.resourceName()).willReturn(resourceName);
given(router.routeRequest(any(HttpMethod.class), any(URIMatch.class))).willReturn(routeMatch);
}
@Test
public void testReceivedHttpRequest() throws Exception {
// given
FullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, GET, "/");
prepareInjectorStubbing();
givenRouting();
given(servableResources.routeProcessor(resourceName)).willReturn(routeProcessor);
// when
handler.channelRead0(ctx, fullHttpRequest);
// then
verify(injector).createChildInjector(moduleCaptor.capture());
Module module = moduleCaptor.getValue();
assertThat(module, is(notNullValue()));
module.configure(binder);
verify(binder).bind(ChannelHandlerContext.class);
verify(abb).toInstance(ctx);
verify(binder).bind(FullHttpRequest.class);
verify(abb).toInstance(fullHttpRequest);
verify(binder).bind(HttpServerRequest.class);
verify(abb).to(HttpServerRequestImpl.class);
verify(binder).bind(HttpServerResponse.class);
verify(abb).to(HttpServerResponseImpl.class);
verifyNoMoreInteractions(binder, abb);
verify(routeProcessor).process(routeMatch, httpRequest1, httpResponse);
}
@Test
public void testNotFound() throws Exception {
// given
FullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, GET, "/");
prepareInjectorStubbing();
givenRouting();
// when
handler.channelRead0(ctx, fullHttpRequest);
// then
verify(httpResponse).sendNotFound();
}
@Test
public void testIOExceptionCaught() throws Exception {
// IO exceptions are ignored at this point. can't do anything about them anyway
IOException ioe = new IOException();
handler.exceptionCaught(ctx, ioe);
verifyNoMoreInteractions(publisher);
verifyNoMoreInteractions(ctx);
}
@Test
public void testExceptionCaught() throws Exception {
given(ctx.writeAndFlush(any())).willReturn(channelFuture);
Throwable t = new Throwable();
handler.exceptionCaught(ctx, t);
// validate that the exception is logged because we
// care about that
verify(publisher).publish(isA(Emergency.class));
verifyNoMoreInteractions(publisher);
verify(ctx).writeAndFlush(responseCaptor.capture());
FullHttpResponse response = responseCaptor.getValue();
assertThat(response.status(), is(HttpResponseStatus.INTERNAL_SERVER_ERROR));
verify(channelFuture).addListener(futureListenerCaptor.capture());
assertThat(futureListenerCaptor.getValue(), is(ChannelFutureListener.CLOSE));
}
@Test
public void testExceptionCaughtCausesException() throws Exception {
//given
RuntimeException second = new RuntimeException();
given(ctx.writeAndFlush(any())).willThrow(second);
//when
Throwable t = new Throwable();
handler.exceptionCaught(ctx, t);
//then
verify(publisher, times(2)).publish(isA(Emergency.class));
}
}