/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package com.github.ambry.rest;
import com.codahale.metrics.MetricRegistry;
import com.github.ambry.router.Callback;
import com.github.ambry.utils.TestUtils;
import com.github.ambry.utils.Utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedWriteHandler;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Tests functionality of {@link NettyResponseChannel}.
* <p/>
* To understand what each {@link TestingUri} is doing, refer to
* {@link MockNettyMessageProcessor#handleRequest(HttpRequest)} and
* {@link MockNettyMessageProcessor#handleContent(HttpContent)}.
*/
public class NettyResponseChannelTest {
private static final Map<RestServiceErrorCode, HttpResponseStatus> REST_ERROR_CODE_TO_HTTP_STATUS = new HashMap<>();
static {
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.BadRequest, HttpResponseStatus.BAD_REQUEST);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.Unauthorized, HttpResponseStatus.UNAUTHORIZED);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.Deleted, HttpResponseStatus.GONE);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.NotFound, HttpResponseStatus.NOT_FOUND);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.ResourceScanInProgress,
HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.ResourceDirty, HttpResponseStatus.FORBIDDEN);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.InternalServerError,
HttpResponseStatus.INTERNAL_SERVER_ERROR);
REST_ERROR_CODE_TO_HTTP_STATUS.put(RestServiceErrorCode.RangeNotSatisfiable,
HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
/**
* Tests the common workflow of the {@link NettyResponseChannel} i.e., add some content to response body via
* {@link NettyResponseChannel#write(ByteBuffer, Callback)} and then complete the response.
* <p/>
* These responses have the header Transfer-Encoding set to chunked.
* @throws Exception
*/
@Test
public void responsesWithTransferEncodingChunkedTest() throws Exception {
String content = "@@randomContent@@@";
String lastContent = "@@randomLastContent@@@";
EmbeddedChannel channel = createEmbeddedChannel();
MockNettyMessageProcessor processor = channel.pipeline().get(MockNettyMessageProcessor.class);
AtomicLong contentIdGenerator = new AtomicLong(0);
final int ITERATIONS = 10;
for (int i = 0; i < ITERATIONS; i++) {
boolean isKeepAlive = i != (ITERATIONS - 1);
HttpRequest httpRequest = RestTestUtils.createRequest(HttpMethod.POST, "/", null);
HttpUtil.setKeepAlive(httpRequest, isKeepAlive);
channel.writeInbound(httpRequest);
ArrayList<String> contents = new ArrayList<>();
for (int j = 0; j <= i; j++) {
String contentToSend = content + contentIdGenerator.getAndIncrement();
channel.writeInbound(createContent(contentToSend, false));
contents.add(contentToSend);
}
channel.writeInbound(createContent(lastContent, true));
verifyCallbacks(processor);
// first outbound has to be response.
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
assertTrue("Response must say 'Transfer-Encoding : chunked'", HttpUtil.isTransferEncodingChunked(response));
// content echoed back.
for (String srcOfTruth : contents) {
String returnedContent = RestTestUtils.getContentString((HttpContent) channel.readOutbound());
assertEquals("Content does not match with expected content", srcOfTruth, returnedContent);
}
// last content echoed back.
String returnedContent = RestTestUtils.getContentString((HttpContent) channel.readOutbound());
assertEquals("Content does not match with expected content", lastContent, returnedContent);
assertTrue("Did not receive end marker", channel.readOutbound() instanceof LastHttpContent);
assertEquals("Unexpected channel state on the server", isKeepAlive, channel.isActive());
}
}
/**
* Tests the common workflow of the {@link NettyResponseChannel} i.e., add some content to response body via
* {@link NettyResponseChannel#write(ByteBuffer, Callback)} and then complete the response.
* <p/>
* These responses have the header Content-Length set.
* @throws Exception
*/
@Test
public void responsesWithContentLengthTest() throws Exception {
EmbeddedChannel channel = createEmbeddedChannel();
MockNettyMessageProcessor processor = channel.pipeline().get(MockNettyMessageProcessor.class);
final int ITERATIONS = 10;
for (int i = 0; i < ITERATIONS; i++) {
boolean isKeepAlive = i != (ITERATIONS - 1);
HttpHeaders httpHeaders = new DefaultHttpHeaders();
httpHeaders.set(MockNettyMessageProcessor.CHUNK_COUNT_HEADER_NAME, i);
HttpRequest httpRequest =
RestTestUtils.createRequest(HttpMethod.POST, TestingUri.ResponseWithContentLength.toString(), httpHeaders);
HttpUtil.setKeepAlive(httpRequest, isKeepAlive);
channel.writeInbound(httpRequest);
verifyCallbacks(processor);
// first outbound has to be response.
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
long contentLength = HttpUtil.getContentLength(response, -1);
assertEquals("Unexpected Content-Length", MockNettyMessageProcessor.CHUNK.length * i, contentLength);
if (contentLength == 0) {
// special case. Since Content-Length is set, the response should be an instance of FullHttpResponse.
assertTrue("Response not instance of FullHttpResponse", response instanceof FullHttpResponse);
} else {
HttpContent httpContent = null;
for (int j = 0; j < i; j++) {
httpContent = (HttpContent) channel.readOutbound();
byte[] returnedContent = httpContent.content().array();
assertArrayEquals("Content does not match with expected content", MockNettyMessageProcessor.CHUNK,
returnedContent);
}
// the last HttpContent should also be an instance of LastHttpContent
assertTrue("The last part of the content is not LastHttpContent", httpContent instanceof LastHttpContent);
}
assertEquals("Unexpected channel state on the server", isKeepAlive, channel.isActive());
}
}
/**
* Checks the case where no body needs to be returned but just a
* {@link RestResponseChannel#onResponseComplete(Exception)} is called on the server. This should return just
* response metadata.
*/
@Test
public void noResponseBodyTest() {
EmbeddedChannel channel = createEmbeddedChannel();
// with Transfer-Encoding:Chunked
HttpRequest httpRequest =
RestTestUtils.createRequest(HttpMethod.GET, TestingUri.ImmediateResponseComplete.toString(), null);
channel.writeInbound(httpRequest);
// There should be a response.
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
assertTrue("Response must say 'Transfer-Encoding : chunked'", HttpUtil.isTransferEncodingChunked(response));
// since this is Transfer-Encoding:chunked, there should be a LastHttpContent
assertTrue("Did not receive end marker", channel.readOutbound() instanceof LastHttpContent);
assertTrue("Channel should be alive", channel.isActive());
// with Content-Length set
HttpHeaders headers = new DefaultHttpHeaders();
headers.set(MockNettyMessageProcessor.CHUNK_COUNT_HEADER_NAME, 0);
httpRequest = RestTestUtils.createRequest(HttpMethod.GET, TestingUri.ImmediateResponseComplete.toString(), headers);
HttpUtil.setKeepAlive(httpRequest, false);
channel.writeInbound(httpRequest);
// There should be a response.
response = (HttpResponse) channel.readOutbound();
assertEquals("Response must have Content-Length set to 0", 0, HttpUtil.getContentLength(response, -1));
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
// since Content-Length is set, the response should be an instance of FullHttpResponse.
assertTrue("Response not instance of FullHttpResponse", response instanceof FullHttpResponse);
assertFalse("Channel should not be alive", channel.isActive());
}
/**
* Performs bad state transitions and verifies that they throw the right exceptions.
* @throws Exception
*/
@Test
public void badStateTransitionsTest() throws Exception {
// write after close.
doBadStateTransitionTest(TestingUri.WriteAfterClose, ClosedChannelException.class);
// modify response data after it has been written to the channel
doBadStateTransitionTest(TestingUri.ModifyResponseMetadataAfterWrite, IllegalStateException.class);
}
/**
* Tests that no exceptions are thrown on repeating idempotent operations. Does <b><i>not</i></b> currently test that
* state changes are idempotent.
*/
@Test
public void idempotentOperationsTest() {
doIdempotentOperationsTest(TestingUri.MultipleClose);
doIdempotentOperationsTest(TestingUri.MultipleOnResponseComplete);
}
/**
* Tests behaviour of various functions of {@link NettyResponseChannel} under write failures.
* @throws Exception
*/
@Test
public void behaviourUnderWriteFailuresTest() throws Exception {
onResponseCompleteUnderWriteFailureTest(TestingUri.ImmediateResponseComplete);
onResponseCompleteUnderWriteFailureTest(TestingUri.OnResponseCompleteWithNonRestException);
// writing to channel with a outbound handler that generates an Exception
try {
String content = "@@randomContent@@@";
MockNettyMessageProcessor processor = new MockNettyMessageProcessor();
ChannelOutboundHandler badOutboundHandler = new ExceptionOutboundHandler();
EmbeddedChannel channel = new EmbeddedChannel(badOutboundHandler, processor);
channel.writeInbound(RestTestUtils.createRequest(HttpMethod.GET, "/", null));
// channel gets closed because of write failure
channel.writeInbound(createContent(content, true));
verifyCallbacks(processor);
fail("Callback for write would have thrown an Exception");
} catch (Exception e) {
assertEquals("Exception not as expected", ExceptionOutboundHandler.EXCEPTION_MESSAGE, e.getMessage());
}
// writing to channel with a outbound handler that generates an Error
MockNettyMessageProcessor processor = new MockNettyMessageProcessor();
EmbeddedChannel channel = new EmbeddedChannel(new ErrorOutboundHandler(), processor);
try {
channel.writeInbound(
RestTestUtils.createRequest(HttpMethod.GET, TestingUri.WriteFailureWithThrowable.toString(), null));
verifyCallbacks(processor);
} catch (Error e) {
assertEquals("Unexpected error", ErrorOutboundHandler.ERROR_MESSAGE, e.getMessage());
}
channel = createEmbeddedChannel();
processor = channel.pipeline().get(MockNettyMessageProcessor.class);
channel.writeInbound(
RestTestUtils.createRequest(HttpMethod.GET, TestingUri.ResponseFailureMidway.toString(), null));
verifyCallbacks(processor);
assertFalse("Channel is not closed at the remote end", channel.isActive());
}
/**
* Asks the server to write more data than the set Content-Length and checks behavior.
* @throws Exception
*/
@Test
public void writeMoreThanContentLengthTest() throws Exception {
doWriteMoreThanContentLengthTest(0);
doWriteMoreThanContentLengthTest(5);
}
/**
* Tests handling of content that is larger than write buffer size. In this test case, the write buffer low and high
* watermarks are requested to be set to 1 and 2 respectively so the content will be written byte by byte into the
* {@link NettyResponseChannel}. This does <b><i>not</i></b> test for the same situation in a async scenario since
* {@link EmbeddedChannel} only provides blocking semantics.
* @throws IOException
*/
@Test
public void fillWriteBufferTest() throws IOException {
String content = "@@randomContent@@@";
String lastContent = "@@randomLastContent@@@";
EmbeddedChannel channel = createEmbeddedChannel();
HttpRequest httpRequest = RestTestUtils.createRequest(HttpMethod.GET, TestingUri.FillWriteBuffer.toString(), null);
HttpUtil.setKeepAlive(httpRequest, false);
channel.writeInbound(httpRequest);
channel.writeInbound(createContent(content, false));
channel.writeInbound(createContent(lastContent, true));
// first outbound has to be response.
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
// content echoed back.
StringBuilder returnedContent = new StringBuilder();
while (returnedContent.length() < content.length()) {
returnedContent.append(RestTestUtils.getContentString((HttpContent) channel.readOutbound()));
}
assertEquals("Content does not match with expected content", content, returnedContent.toString());
// last content echoed back.
StringBuilder returnedLastContent = new StringBuilder();
while (returnedLastContent.length() < lastContent.length()) {
returnedLastContent.append(RestTestUtils.getContentString((HttpContent) channel.readOutbound()));
}
assertEquals("Content does not match with expected content", lastContent, returnedLastContent.toString());
assertFalse("Channel not closed on the server", channel.isActive());
}
/**
* Sends a request with certain headers that will copied into the response. Checks the response for those headers to
* see that values match.
* @throws ParseException
*/
@Test
public void headersPresenceTest() throws ParseException {
HttpRequest request = createRequestWithHeaders(HttpMethod.GET, TestingUri.CopyHeaders.toString());
HttpUtil.setKeepAlive(request, false);
EmbeddedChannel channel = createEmbeddedChannel();
channel.writeInbound(request);
HttpResponse response = (HttpResponse) channel.readOutbound();
assertFalse("Channel not closed on the server", channel.isActive());
checkHeaders(request, response);
}
/**
* Sends null input to {@link NettyResponseChannel#setHeader(String, Object)} (through
* {@link MockNettyMessageProcessor}) and tests for reaction.
*/
@Test
public void nullHeadersSetTest() {
HttpRequest request = createRequestWithHeaders(HttpMethod.GET, TestingUri.SetNullHeader.toString());
HttpUtil.setKeepAlive(request, false);
EmbeddedChannel channel = createEmbeddedChannel();
channel.writeInbound(request);
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.ACCEPTED, response.status());
assertFalse("Channel not closed on the server", channel.isActive());
}
/**
* Tries different exception scenarios for {@link NettyResponseChannel#setRequest(NettyRequest)}.
*/
@Test
public void setRequestTest() {
HttpRequest request = createRequestWithHeaders(HttpMethod.GET, TestingUri.SetRequest.toString());
HttpUtil.setKeepAlive(request, false);
EmbeddedChannel channel = createEmbeddedChannel();
channel.writeInbound(request);
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.ACCEPTED, response.status());
assertFalse("Channel not closed on the server", channel.isActive());
}
/**
* Tests setting of different available {@link ResponseStatus} codes and sees that they are recognized and converted
* in {@link NettyResponseChannel}.
* <p/>
* If this test fails, a case for conversion probably needs to be added in {@link NettyResponseChannel}.
*/
@Test
public void setStatusTest() {
// ask for every status to be set
for (ResponseStatus expectedResponseStatus : ResponseStatus.values()) {
HttpRequest request = createRequestWithHeaders(HttpMethod.GET, TestingUri.SetStatus.toString());
request.headers().set(MockNettyMessageProcessor.STATUS_HEADER_NAME, expectedResponseStatus);
HttpUtil.setKeepAlive(request, false);
EmbeddedChannel channel = createEmbeddedChannel();
channel.writeInbound(request);
// pull but discard response
channel.readOutbound();
assertFalse("Channel not closed on the server", channel.isActive());
}
// check if all the ResponseStatus codes were recognized.
String metricName = MetricRegistry.name(NettyResponseChannel.class, "UnknownResponseStatusCount");
long metricCount = MockNettyMessageProcessor.METRIC_REGISTRY.getCounters().get(metricName).getCount();
assertEquals("Some of the ResponseStatus codes were not recognized", 0, metricCount);
}
/**
* Tests that error responses are correctly formed.
*/
@Test
public void errorResponseTest() {
EmbeddedChannel channel = createEmbeddedChannel();
for (Map.Entry<RestServiceErrorCode, HttpResponseStatus> entry : REST_ERROR_CODE_TO_HTTP_STATUS.entrySet()) {
HttpHeaders httpHeaders = new DefaultHttpHeaders();
httpHeaders.set(MockNettyMessageProcessor.REST_SERVICE_ERROR_CODE_HEADER_NAME, entry.getKey());
channel.writeInbound(
RestTestUtils.createRequest(HttpMethod.HEAD, TestingUri.OnResponseCompleteWithRestException.toString(),
httpHeaders));
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", entry.getValue(), response.status());
boolean containsFailureReasonHeader = response.headers().contains(NettyResponseChannel.FAILURE_REASON_HEADER);
if (entry.getValue() == HttpResponseStatus.BAD_REQUEST) {
assertTrue("Could not find failure reason header.", containsFailureReasonHeader);
} else {
assertFalse("Should not have found failure reason header.", containsFailureReasonHeader);
}
if (response instanceof FullHttpResponse) {
// assert that there is no content
assertEquals("The response should not contain content", 0,
((FullHttpResponse) response).content().readableBytes());
} else {
HttpContent content = (HttpContent) channel.readOutbound();
assertTrue("End marker should be received", content instanceof LastHttpContent);
}
assertNull("There should be no more data in the channel", channel.readOutbound());
boolean shouldBeAlive = !NettyResponseChannel.CLOSE_CONNECTION_ERROR_STATUSES.contains(entry.getValue());
assertEquals("Channel state (open/close) not as expected", shouldBeAlive, channel.isActive());
assertEquals("Connection header should be consistent with channel state", shouldBeAlive,
HttpUtil.isKeepAlive(response));
if (!shouldBeAlive) {
channel = createEmbeddedChannel();
}
}
channel.close();
}
/**
* Tests keep-alive for different HTTP methods and error statuses.
*/
@Test
public void keepAliveTest() {
HttpMethod[] HTTP_METHODS = {HttpMethod.POST, HttpMethod.PUT, HttpMethod.GET, HttpMethod.HEAD, HttpMethod.DELETE};
EmbeddedChannel channel = createEmbeddedChannel();
for (HttpMethod httpMethod : HTTP_METHODS) {
for (Map.Entry<RestServiceErrorCode, HttpResponseStatus> entry : REST_ERROR_CODE_TO_HTTP_STATUS.entrySet()) {
channel = doKeepAliveTest(channel, httpMethod, entry.getKey(), entry.getValue());
}
channel = doKeepAliveTest(channel, httpMethod, null, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
channel.close();
}
/**
* Tests that the underlying network channel is closed when {@link NettyResponseChannel#close()} is called.
*/
@Test
public void closeTest() {
// request is keep-alive by default.
HttpRequest request = createRequestWithHeaders(HttpMethod.GET, TestingUri.Close.toString());
EmbeddedChannel channel = createEmbeddedChannel();
channel.writeInbound(request);
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.INTERNAL_SERVER_ERROR, response.status());
assertFalse("Inconsistent value for Connection header", HttpUtil.isKeepAlive(response));
// drain the channel of content.
while (channel.readOutbound() != null) {
}
assertFalse("Channel should be closed", channel.isOpen());
}
// helpers
// general
/**
* Creates {@link HttpContent} wrapping the {@code content}.
* @param content the content to wrap.
* @param isLast {@code true} if this is the last piece of content. {@code false} otherwise.
* @return a {@link HttpContent} wrapping the {@code content}.
*/
private HttpContent createContent(String content, boolean isLast) {
ByteBuf buf = Unpooled.copiedBuffer(content.getBytes());
if (isLast) {
return new DefaultLastHttpContent(buf);
} else {
return new DefaultHttpContent(buf);
}
}
/**
* Creates a new {@link EmbeddedChannel} with a {@link ChunkedWriteHandler} and {@link MockNettyMessageProcessor} in
* the pipeline.
* @return the created {@link EmbeddedChannel}.
*/
private EmbeddedChannel createEmbeddedChannel() {
ChunkedWriteHandler chunkedWriteHandler = new ChunkedWriteHandler();
MockNettyMessageProcessor processor = new MockNettyMessageProcessor();
return new EmbeddedChannel(chunkedWriteHandler, processor);
}
/**
* Verifies any callbacks queued in the {@code processor}.
* @param processor the {@link MockNettyMessageProcessor} that contains the callbacks that need to be verified.
* @throws Exception
*/
private void verifyCallbacks(MockNettyMessageProcessor processor) throws Exception {
if (processor == null) {
assertNotNull("There is no MockNettyMessageProcessor in the channel", processor);
}
for (ChannelWriteCallback callback : processor.writeCallbacksToVerify) {
callback.compareWithFuture();
if (callback.exception != null) {
throw callback.exception;
}
}
}
// badStateTransitionsTest() helpers
/**
* Creates a channel and sends the request to the {@link EmbeddedChannel}. Checks for an exception and verifies the
* exception class matches. If {@code exceptionClass} is {@link RestServiceException}, then checks the provided
* {@link RestServiceErrorCode}.
* @param uri the uri to hit.
* @param exceptionClass the class of the exception expected.
* @throws Exception
*/
private void doBadStateTransitionTest(TestingUri uri, Class exceptionClass) throws Exception {
EmbeddedChannel channel = createEmbeddedChannel();
MockNettyMessageProcessor processor = channel.pipeline().get(MockNettyMessageProcessor.class);
try {
channel.writeInbound(RestTestUtils.createRequest(HttpMethod.GET, uri.toString(), null));
verifyCallbacks(processor);
fail("This test was expecting the handler in the channel to throw an exception");
} catch (Exception e) {
if (!exceptionClass.isInstance(e)) {
throw e;
}
}
}
// idempotentOperationsTest() helpers
/**
* Checks that idempotent operations do not throw exceptions when called multiple times. Does <b><i>not</i></b>
* currently test that state changes are idempotent.
* @param uri the uri to be hit.
*/
private void doIdempotentOperationsTest(TestingUri uri) {
EmbeddedChannel channel = createEmbeddedChannel();
// no exceptions.
channel.writeInbound(RestTestUtils.createRequest(HttpMethod.GET, uri.toString(), null));
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
}
/**
* Checks that no exceptions are thrown by {@link RestResponseChannel#onResponseComplete(Exception)} when
* there are write failures.
* @param uri the uri to hit.
*/
private void onResponseCompleteUnderWriteFailureTest(TestingUri uri) {
MockNettyMessageProcessor processor = new MockNettyMessageProcessor();
ExceptionOutboundHandler exceptionOutboundHandler = new ExceptionOutboundHandler();
EmbeddedChannel channel = new EmbeddedChannel(exceptionOutboundHandler, processor);
// no exception because onResponseComplete() swallows it.
channel.writeInbound(RestTestUtils.createRequest(HttpMethod.GET, uri.toString(), null));
assertFalse("Channel is not closed at the remote end", channel.isActive());
}
// writeMoreThanContentLengthTest() helpers.
/**
* Asks the server to write more data than the set Content-Length and checks behavior.
* @param chunkCount the number of chunks of {@link MockNettyMessageProcessor#CHUNK} to use to set Content-Length.
* @throws Exception
*/
private void doWriteMoreThanContentLengthTest(int chunkCount) throws Exception {
EmbeddedChannel channel = createEmbeddedChannel();
MockNettyMessageProcessor processor = channel.pipeline().get(MockNettyMessageProcessor.class);
HttpHeaders httpHeaders = new DefaultHttpHeaders();
httpHeaders.set(MockNettyMessageProcessor.CHUNK_COUNT_HEADER_NAME, chunkCount);
HttpRequest httpRequest =
RestTestUtils.createRequest(HttpMethod.POST, TestingUri.WriteMoreThanContentLength.toString(), httpHeaders);
HttpUtil.setKeepAlive(httpRequest, true);
channel.writeInbound(httpRequest);
try {
verifyCallbacks(processor);
fail("One of the callbacks should have failed because the data written was more than Content-Length");
} catch (IllegalStateException e) {
// expected. Nothing to do.
}
// It doesn't matter what the response is - because it may either fail or succeed depending on certain race
// conditions. What matters is that the programming error is caught appropriately by NettyResponseChannel and it
// makes a callback with the right exception.
while (channel.readOutbound() != null) {
}
channel.close();
}
// headersPresenceTest() helpers
/**
* Creates a {@link HttpRequest} with some headers set that will be checked on response.
* @param httpMethod the {@link HttpMethod} desired.
* @param uri the URI to hit.
* @return a link {@link HttpRequest} with some headers set.
*/
private HttpRequest createRequestWithHeaders(HttpMethod httpMethod, String uri) {
long currentTime = System.currentTimeMillis();
HttpRequest request = RestTestUtils.createRequest(httpMethod, uri, null);
request.headers().set(HttpHeaderNames.CONTENT_TYPE, "dummy/content-type");
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, 100);
request.headers().set(HttpHeaderNames.LOCATION, "dummyLocation");
request.headers().set(HttpHeaderNames.LAST_MODIFIED, new Date(currentTime));
request.headers().set(HttpHeaderNames.EXPIRES, new Date(currentTime + 1));
request.headers().set(HttpHeaderNames.CACHE_CONTROL, "dummyCacheControl");
request.headers().set(HttpHeaderNames.PRAGMA, "dummyPragma");
request.headers().set(HttpHeaderNames.DATE, new Date(currentTime + 2));
request.headers().set(MockNettyMessageProcessor.CUSTOM_HEADER_NAME, "customHeaderValue");
return request;
}
/**
* Checks the headers in the response match those in the request.
* @param request the {@link HttpRequest} with the original value of the headers.
* @param response the {@link HttpResponse} that should have the same value for some headers in {@code request}.
* @throws ParseException
*/
private void checkHeaders(HttpRequest request, HttpResponse response) throws ParseException {
assertEquals("Unexpected response status", HttpResponseStatus.ACCEPTED, response.status());
assertEquals(HttpHeaderNames.CONTENT_TYPE + " does not match", request.headers().get(HttpHeaderNames.CONTENT_TYPE),
response.headers().get(HttpHeaderNames.CONTENT_TYPE));
assertEquals(HttpHeaderNames.CONTENT_LENGTH + " does not match",
request.headers().get(HttpHeaderNames.CONTENT_LENGTH), response.headers().get(HttpHeaderNames.CONTENT_LENGTH));
assertEquals(HttpHeaderNames.LOCATION + " does not match", request.headers().get(HttpHeaderNames.LOCATION),
response.headers().get(HttpHeaderNames.LOCATION));
assertEquals(HttpHeaderNames.LAST_MODIFIED + " does not match",
request.headers().getTimeMillis(HttpHeaderNames.LAST_MODIFIED),
response.headers().getTimeMillis(HttpHeaderNames.LAST_MODIFIED));
assertEquals(HttpHeaderNames.EXPIRES + " does not match", request.headers().getTimeMillis(HttpHeaderNames.EXPIRES),
response.headers().getTimeMillis(HttpHeaderNames.EXPIRES));
assertEquals(HttpHeaderNames.CACHE_CONTROL + " does not match",
request.headers().get(HttpHeaderNames.CACHE_CONTROL), response.headers().get(HttpHeaderNames.CACHE_CONTROL));
assertEquals(HttpHeaderNames.PRAGMA + " does not match", request.headers().get(HttpHeaderNames.PRAGMA),
response.headers().get(HttpHeaderNames.PRAGMA));
assertEquals(HttpHeaderNames.DATE + " does not match", request.headers().getTimeMillis(HttpHeaderNames.DATE),
response.headers().getTimeMillis(HttpHeaderNames.DATE));
assertEquals(MockNettyMessageProcessor.CUSTOM_HEADER_NAME + " does not match",
request.headers().get(MockNettyMessageProcessor.CUSTOM_HEADER_NAME),
response.headers().get(MockNettyMessageProcessor.CUSTOM_HEADER_NAME));
}
// keepAliveTest() helpers.
/**
* Does the keep-alive test by setting the {@link HttpHeaderNames#CONNECTION} to its two possible values and tests
* that the response has the appropriate value for the {@link HttpHeaderNames#CONNECTION}.
* @param channel the {@link EmbeddedChannel} to send the request over.
* @param httpMethod the {@link HttpMethod} of the request.
* @param errorCode the {@link RestServiceErrorCode} to induce at {@link MockNettyMessageProcessor}. {@code null} if
* {@link TestingUri#OnResponseCompleteWithNonRestException} is desired.
* @param expectedResponseStatus the expected {@link HttpResponseStatus} from remote.
* @return the {@link EmbeddedChannel} to use once this function is complete. If the channel did not close, this
* function will return the {@code channel} instance that was passed, otherwise it returns a new channel.
*/
private EmbeddedChannel doKeepAliveTest(EmbeddedChannel channel, HttpMethod httpMethod,
RestServiceErrorCode errorCode, HttpResponseStatus expectedResponseStatus) {
boolean keepAlive = true;
for (int i = 0; i < 2; i++) {
HttpHeaders httpHeaders = new DefaultHttpHeaders();
TestingUri uri = TestingUri.OnResponseCompleteWithNonRestException;
if (errorCode != null) {
uri = TestingUri.OnResponseCompleteWithRestException;
httpHeaders.set(MockNettyMessageProcessor.REST_SERVICE_ERROR_CODE_HEADER_NAME, errorCode);
}
if (!keepAlive) {
httpHeaders.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
channel.writeInbound(RestTestUtils.createRequest(httpMethod, uri.toString(), httpHeaders));
HttpResponse response = (HttpResponse) channel.readOutbound();
assertEquals("Unexpected response status", expectedResponseStatus, response.status());
if (!(response instanceof FullHttpResponse)) {
// empty the channel
while (channel.readOutbound() != null) {
}
}
boolean shouldBeAlive = keepAlive && !httpMethod.equals(HttpMethod.POST) && !httpMethod.equals(HttpMethod.PUT)
&& !NettyResponseChannel.CLOSE_CONNECTION_ERROR_STATUSES.contains(expectedResponseStatus);
assertEquals("Channel state (open/close) not as expected", shouldBeAlive, channel.isActive());
assertEquals("Connection header should be consistent with channel state", shouldBeAlive,
HttpUtil.isKeepAlive(response));
if (!shouldBeAlive) {
channel.close();
channel = createEmbeddedChannel();
}
keepAlive = !keepAlive;
}
return channel;
}
}
/**
* List of all the testing URIs.
*/
enum TestingUri {
/**
* When this request is received, {@link NettyResponseChannel#close()} is called immediately.
*/
Close, /**
* When this request is received, headers from the request are copied into the response channel.
*/
CopyHeaders, /**
* When this request is received, {@link RestResponseChannel#onResponseComplete(Exception)} is called
* immediately with null {@code cause}.
*/
ImmediateResponseComplete, /**
* Reduces the write buffer low and high watermarks to 1 and 2 bytes respectively in
* {@link io.netty.channel.ChannelConfig} so that data is written to the channel byte by byte. This simulates filling
* up of write buffer (but does not test async writing and flushing since {@link EmbeddedChannel} is blocking).
*/
FillWriteBuffer, /**
* When this request is received, some data is initially written to the channel via
* {@link NettyResponseChannel#write(ByteBuffer, Callback)} . An attempt to modify response headers (metadata) is made
* after this.
*/
ModifyResponseMetadataAfterWrite, /**
* When this request is received, {@link NettyResponseChannel#close()} is called multiple times.
*/
MultipleClose, /**
* When this request is received, {@link RestResponseChannel#onResponseComplete(Exception)} is called
* multiple times.
*/
MultipleOnResponseComplete, /**
* When this request is received, {@link RestResponseChannel#onResponseComplete(Exception)} is called
* immediately with a {@link RestServiceException} as {@code cause}. The exception message and error code is the
* {@link RestServiceErrorCode} passed in as the value of the header
* {@link MockNettyMessageProcessor#REST_SERVICE_ERROR_CODE_HEADER_NAME}.
*/
OnResponseCompleteWithRestException, /**
* When this request is received, {@link RestResponseChannel#onResponseComplete(Exception)} is called
* immediately with a {@link RuntimeException} as {@code cause}. The exception message is the URI string.
*/
OnResponseCompleteWithNonRestException, /**
* Response sending fails midway through a write.
*/
ResponseFailureMidway, /**
* When this request is received, a response with {@link RestUtils.Headers#CONTENT_LENGTH} set is returned.
* The value of the header {@link MockNettyMessageProcessor#CHUNK_COUNT_HEADER_NAME} is used to determine the number
* of chunks (each equal to {@link MockNettyMessageProcessor#CHUNK}) to return.
* <p/>
* The {@link RestUtils.Headers#CONTENT_LENGTH} is equal to the value in
* {@link MockNettyMessageProcessor#CHUNK_COUNT_HEADER_NAME} times the length of
* {@link MockNettyMessageProcessor#CHUNK}
*/
ResponseWithContentLength, /**
* When this request is received, {@link NettyResponseChannel#setHeader(String, Object)} is attempted with null
* arguments. If these calls don't fail, we report an error.
*/
SetNullHeader, /**
* Tests setting of a {@link NettyRequest} in {@link NettyResponseChannel}.
*/
SetRequest, /**
* Requests a certain status to be set.
*/
SetStatus, /**
* When this request is received, the {@link NettyResponseChannel} is closed and then a write operation is attempted.
*/
WriteAfterClose, /**
* Fail a write with a {@link Throwable} to test reactions.
*/
WriteFailureWithThrowable, /**
* When this request is received, a response with {@link RestUtils.Headers#CONTENT_LENGTH} set is returned.
* The value of the header {@link MockNettyMessageProcessor#CHUNK_COUNT_HEADER_NAME} is used to determine the number
* of chunks (each equal to {@link MockNettyMessageProcessor#CHUNK}) to add to the response channel. The chunks added
* is one more than the value of {@link MockNettyMessageProcessor#CHUNK_COUNT_HEADER_NAME}. The last chunk write is
* checked for error.
* <p/>
* The {@link RestUtils.Headers#CONTENT_LENGTH} is equal to the value in
* {@link MockNettyMessageProcessor#CHUNK_COUNT_HEADER_NAME} times the length of
* {@link MockNettyMessageProcessor#CHUNK}
*/
WriteMoreThanContentLength, /**
* Catch all TestingUri.
*/
Unknown;
/**
* Converts the uri specified by the input string into a {@link TestingUri}.
* @param uri the TestingUri as a string.
* @return the uri requested as a valid {@link TestingUri} if uri is known, otherwise returns {@link #Unknown}
*/
public static TestingUri getTestingURI(String uri) {
try {
return TestingUri.valueOf(uri);
} catch (IllegalArgumentException e) {
return TestingUri.Unknown;
}
}
}
/**
* A test handler that forms the pipeline of the {@link EmbeddedChannel} used in tests.
* <p/>
* Exposes some URI strings through which a predefined flow can be executed and verified.
*/
class MockNettyMessageProcessor extends SimpleChannelInboundHandler<HttpObject> {
static final MetricRegistry METRIC_REGISTRY = new MetricRegistry();
static final String CUSTOM_HEADER_NAME = "customHeader";
static final String STATUS_HEADER_NAME = "status";
static final String REST_SERVICE_ERROR_CODE_HEADER_NAME = "restServiceErrorCode";
// CHUNK and CHUNK_COUNT HEADER_NAME together help in Content-Length tests.
// If a test sets CHUNK_COUNT_HEADER_NAME to 3,
// 1. The Content-Length is set to 3 * CHUNK.length
// 2. The content is sent in two chunks both of which contain the same data.
// 3. The last chunk will be sent as LastHttpContent.
static final byte[] CHUNK = TestUtils.getRandomBytes(1024);
static final String CHUNK_COUNT_HEADER_NAME = "chunkCount";
// the write callbacks to verify if any. This is reset at the beginning of every request.
final List<ChannelWriteCallback> writeCallbacksToVerify = new ArrayList<>();
private ChannelHandlerContext ctx;
private NettyRequest request;
private NettyResponseChannel restResponseChannel;
private NettyMetrics nettyMetrics;
@Override
public void channelActive(ChannelHandlerContext ctx) {
this.ctx = ctx;
nettyMetrics = new NettyMetrics(METRIC_REGISTRY);
RestRequestMetricsTracker.setDefaults(METRIC_REGISTRY);
}
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject obj) throws Exception {
if (obj != null && obj instanceof HttpRequest) {
if (obj.decoderResult().isSuccess()) {
handleRequest((HttpRequest) obj);
} else {
throw new RestServiceException("Malformed request received - " + obj, RestServiceErrorCode.MalformedRequest);
}
} else if (obj != null && obj instanceof HttpContent) {
handleContent((HttpContent) obj);
} else {
throw new RestServiceException("HttpObject received is null or not of a known type",
RestServiceErrorCode.MalformedRequest);
}
}
/**
* Handles a {@link HttpRequest}. If content is awaited, handles some state maintenance. Else handles the request
* according to a predefined flow based on the uri.
* @param httpRequest the {@link HttpRequest} that needs to be handled.
* @throws Exception
*/
private void handleRequest(HttpRequest httpRequest) throws Exception {
writeCallbacksToVerify.clear();
request = new NettyRequest(httpRequest, ctx.channel(), nettyMetrics);
restResponseChannel = new NettyResponseChannel(ctx, nettyMetrics);
restResponseChannel.setRequest(request);
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, "application/octet-stream");
TestingUri uri = TestingUri.getTestingURI(request.getUri());
switch (uri) {
case Close:
restResponseChannel.close();
assertFalse("Request channel is not closed", request.isOpen());
break;
case CopyHeaders:
copyHeaders(httpRequest);
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
break;
case ImmediateResponseComplete:
int chunkCount = httpRequest.headers().getInt(CHUNK_COUNT_HEADER_NAME, -1);
if (chunkCount > 0) {
restResponseChannel.onResponseComplete(new RestServiceException(
"Invalid value for header : [" + CHUNK_COUNT_HEADER_NAME + "]. Can only be 0 for [/" + uri + "]",
RestServiceErrorCode.BadRequest));
} else if (chunkCount == 0) {
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, 0);
}
restResponseChannel.onResponseComplete(null);
assertEquals("ResponseStatus differs from default", ResponseStatus.Ok, restResponseChannel.getStatus());
assertFalse("Request channel is not closed", request.isOpen());
break;
case FillWriteBuffer:
WriteBufferWaterMark writeBufferWaterMark = new WriteBufferWaterMark(1, 2);
ctx.channel().config().setWriteBufferWaterMark(writeBufferWaterMark);
break;
case ModifyResponseMetadataAfterWrite:
restResponseChannel.write(ByteBuffer.wrap(new byte[0]), null);
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, "application/octet-stream");
break;
case MultipleClose:
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
restResponseChannel.close();
restResponseChannel.close();
break;
case MultipleOnResponseComplete:
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
restResponseChannel.onResponseComplete(null);
break;
case OnResponseCompleteWithRestException:
String errorCodeStr = (String) request.getArgs().get(REST_SERVICE_ERROR_CODE_HEADER_NAME);
RestServiceErrorCode errorCode = RestServiceErrorCode.valueOf(errorCodeStr);
restResponseChannel.onResponseComplete(new RestServiceException(errorCodeStr, errorCode));
assertEquals("ResponseStatus does not reflect error", ResponseStatus.getResponseStatus(errorCode),
restResponseChannel.getStatus());
assertFalse("Request channel is not closed", request.isOpen());
break;
case OnResponseCompleteWithNonRestException:
restResponseChannel.onResponseComplete(
new RuntimeException(TestingUri.OnResponseCompleteWithNonRestException.toString()));
assertEquals("ResponseStatus does not reflect error", ResponseStatus.InternalServerError,
restResponseChannel.getStatus());
assertFalse("Request channel is not closed", request.isOpen());
break;
case ResponseFailureMidway:
ChannelWriteCallback callback = new ChannelWriteCallback();
callback.setFuture(
restResponseChannel.write(ByteBuffer.wrap(TestingUri.ResponseFailureMidway.toString().getBytes()),
callback));
writeCallbacksToVerify.add(callback);
restResponseChannel.onResponseComplete(new Exception());
// this should close the channel and the test will check for that.
break;
case ResponseWithContentLength:
chunkCount = httpRequest.headers().getInt(CHUNK_COUNT_HEADER_NAME, -1);
if (chunkCount == -1) {
restResponseChannel.onResponseComplete(
new RestServiceException("Request should contain header : [" + CHUNK_COUNT_HEADER_NAME + "]",
RestServiceErrorCode.BadRequest));
} else {
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, chunkCount * CHUNK.length);
if (chunkCount == 0) {
// special case check
callback = new ChannelWriteCallback();
callback.setFuture(restResponseChannel.write(ByteBuffer.allocate(0), callback));
writeCallbacksToVerify.add(callback);
} else {
for (int i = 0; i < chunkCount; i++) {
callback = new ChannelWriteCallback();
callback.setFuture(restResponseChannel.write(ByteBuffer.wrap(CHUNK), callback));
writeCallbacksToVerify.add(callback);
}
}
restResponseChannel.onResponseComplete(null);
}
assertFalse("Request channel is not closed", request.isOpen());
break;
case SetNullHeader:
setNullHeaders();
break;
case SetRequest:
setRequestTest();
break;
case SetStatus:
restResponseChannel.setStatus(ResponseStatus.valueOf(httpRequest.headers().get(STATUS_HEADER_NAME)));
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
break;
case WriteAfterClose:
restResponseChannel.close();
assertFalse("Request channel is not closed", request.isOpen());
callback = new ChannelWriteCallback();
callback.setFuture(
restResponseChannel.write(ByteBuffer.wrap(TestingUri.WriteAfterClose.toString().getBytes()), callback));
writeCallbacksToVerify.add(callback);
break;
case WriteFailureWithThrowable:
callback = new ChannelWriteCallback();
callback.setFuture(
restResponseChannel.write(ByteBuffer.wrap(TestingUri.WriteFailureWithThrowable.toString().getBytes()),
callback));
writeCallbacksToVerify.add(callback);
break;
case WriteMoreThanContentLength:
chunkCount = httpRequest.headers().getInt(CHUNK_COUNT_HEADER_NAME, -1);
if (chunkCount == -1) {
restResponseChannel.onResponseComplete(
new RestServiceException("Request should contain header : [" + CHUNK_COUNT_HEADER_NAME + "]",
RestServiceErrorCode.BadRequest));
} else {
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, chunkCount * CHUNK.length);
// write one more chunk than required.
for (int i = 0; i <= chunkCount; i++) {
callback = new ChannelWriteCallback();
callback.setFuture(restResponseChannel.write(ByteBuffer.wrap(CHUNK), callback));
writeCallbacksToVerify.add(callback);
}
restResponseChannel.onResponseComplete(null);
}
break;
}
}
/**
* Handles a {@link HttpContent}. Checks state and echoes back the content.
* @param httpContent the {@link HttpContent} that needs to be handled.
* @throws Exception
*/
private void handleContent(HttpContent httpContent) throws Exception {
if (request != null) {
boolean isLast = httpContent instanceof LastHttpContent;
ByteBuffer content = ByteBuffer.wrap(httpContent.content().array());
ChannelWriteCallback callback = new ChannelWriteCallback();
callback.setFuture(restResponseChannel.write(content, callback));
writeCallbacksToVerify.add(callback);
if (isLast) {
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
}
} else {
throw new RestServiceException("Received data without a request", RestServiceErrorCode.InvalidRequestState);
}
}
/**
* Copies headers from request to response.
* @param httpRequest the {@link HttpRequest} to copy headers from.
* @throws ParseException
* @throws RestServiceException
*/
private void copyHeaders(HttpRequest httpRequest) throws ParseException, RestServiceException {
restResponseChannel.setStatus(ResponseStatus.Accepted);
assertEquals("ResponseStatus differs from what was set", ResponseStatus.Accepted, restResponseChannel.getStatus());
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE,
httpRequest.headers().get(HttpHeaderNames.CONTENT_TYPE));
assertEquals("Value of [" + RestUtils.Headers.CONTENT_TYPE + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.CONTENT_TYPE),
restResponseChannel.getHeader(HttpHeaderNames.CONTENT_TYPE.toString()));
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH,
Long.parseLong(httpRequest.headers().get(HttpHeaderNames.CONTENT_LENGTH)));
assertEquals("Value of [" + RestUtils.Headers.CONTENT_LENGTH + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.CONTENT_LENGTH),
restResponseChannel.getHeader(HttpHeaderNames.CONTENT_LENGTH.toString()));
restResponseChannel.setHeader(RestUtils.Headers.LOCATION, httpRequest.headers().get(HttpHeaderNames.LOCATION));
assertEquals("Value of [" + RestUtils.Headers.LOCATION + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.LOCATION),
restResponseChannel.getHeader(HttpHeaderNames.LOCATION.toString()));
restResponseChannel.setHeader(RestUtils.Headers.LAST_MODIFIED,
new Date(httpRequest.headers().getTimeMillis(HttpHeaderNames.LAST_MODIFIED)));
assertEquals("Value of [" + RestUtils.Headers.LAST_MODIFIED + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.LAST_MODIFIED),
restResponseChannel.getHeader(HttpHeaderNames.LAST_MODIFIED.toString()));
restResponseChannel.setHeader(RestUtils.Headers.EXPIRES,
new Date(httpRequest.headers().getTimeMillis(HttpHeaderNames.EXPIRES)));
assertEquals("Value of [" + RestUtils.Headers.EXPIRES + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.EXPIRES),
restResponseChannel.getHeader(HttpHeaderNames.EXPIRES.toString()));
restResponseChannel.setHeader(RestUtils.Headers.CACHE_CONTROL,
httpRequest.headers().get(HttpHeaderNames.CACHE_CONTROL));
assertEquals("Value of [" + RestUtils.Headers.CACHE_CONTROL + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.CACHE_CONTROL),
restResponseChannel.getHeader(HttpHeaderNames.CACHE_CONTROL.toString()));
restResponseChannel.setHeader(RestUtils.Headers.PRAGMA, httpRequest.headers().get(HttpHeaderNames.PRAGMA));
assertEquals("Value of [" + RestUtils.Headers.PRAGMA + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.PRAGMA),
restResponseChannel.getHeader(HttpHeaderNames.PRAGMA.toString()));
restResponseChannel.setHeader(RestUtils.Headers.DATE,
new Date(httpRequest.headers().getTimeMillis(HttpHeaderNames.DATE)));
assertEquals("Value of [" + RestUtils.Headers.DATE + "] differs from what was set",
httpRequest.headers().get(HttpHeaderNames.DATE),
restResponseChannel.getHeader(HttpHeaderNames.DATE.toString()));
restResponseChannel.setHeader(CUSTOM_HEADER_NAME, httpRequest.headers().get(CUSTOM_HEADER_NAME));
assertEquals("Value of [" + CUSTOM_HEADER_NAME + "] differs from what was set",
httpRequest.headers().get(CUSTOM_HEADER_NAME), restResponseChannel.getHeader(CUSTOM_HEADER_NAME));
}
/**
* Tries to set null headers in the {@link NettyResponseChannel}. If the operation does not fail, reports an error.
* @throws RestServiceException
*/
private void setNullHeaders() throws RestServiceException {
ResponseStatus status = ResponseStatus.Accepted;
try {
// headerName null.
try {
restResponseChannel.setHeader(null, "dummyHeaderValue");
status = ResponseStatus.InternalServerError;
fail("Call to setHeader with null values succeeded. It should have not");
} catch (IllegalArgumentException e) {
// expected. nothing to do.
}
// headerValue null.
try {
restResponseChannel.setHeader("dummyHeaderName", null);
status = ResponseStatus.InternalServerError;
fail("Call to setHeader with null values succeeded. It should have not");
} catch (IllegalArgumentException e) {
// expected. nothing to do.
}
// headerName and headerValue null.
try {
restResponseChannel.setHeader(null, null);
status = ResponseStatus.InternalServerError;
fail("Call to setHeader with null values succeeded. It should have not");
} catch (IllegalArgumentException e) {
// expected. nothing to do.
}
} finally {
restResponseChannel.setStatus(status);
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
}
}
/**
* Tries different exception scenarios for {@link NettyResponseChannel#setRequest(NettyRequest)}.
* @throws RestServiceException
*/
private void setRequestTest() throws RestServiceException {
ResponseStatus status = ResponseStatus.Accepted;
restResponseChannel = new NettyResponseChannel(ctx, new NettyMetrics(new MetricRegistry()));
try {
try {
restResponseChannel.setRequest(null);
status = ResponseStatus.InternalServerError;
fail("Tried to set null request yet no exception was thrown");
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
restResponseChannel.setRequest(request);
try {
restResponseChannel.setRequest(request);
status = ResponseStatus.InternalServerError;
fail("Tried to reset request and no exception was thrown");
} catch (IllegalStateException e) {
// expected. Nothing to do.
}
} finally {
restResponseChannel.setStatus(status);
restResponseChannel.onResponseComplete(null);
assertFalse("Request channel is not closed", request.isOpen());
}
}
}
/**
* A {@link ChannelOutboundHandler} that throws exceptions on write.
*/
class ExceptionOutboundHandler extends ChannelOutboundHandlerAdapter {
protected static String EXCEPTION_MESSAGE = "@@randomExceptionMessage@@";
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
throw new Exception(EXCEPTION_MESSAGE);
}
}
/**
* A {@link ChannelOutboundHandler} that throws errors on write.
*/
class ErrorOutboundHandler extends ChannelOutboundHandlerAdapter {
protected static String ERROR_MESSAGE = "@@randomErrorMessage@@";
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
throw new Error(ERROR_MESSAGE);
}
}
/**
* Class that can be used to receive callbacks from {@link NettyResponseChannel}.
* <p/>
* On callback, stores the result and exception to be retrieved for later use.
*/
class ChannelWriteCallback implements Callback<Long> {
/**
* Contains the result of the operation for which this was set as callback.
* If there was no result or if this was called before callback is received, it will be null
*/
public Long result = null;
/**
* Stores any exception thrown by the operation for which this was set as callback.
* If there was no exception or if this was called before callback is received, it will be null.
*/
public Exception exception = null;
private CountDownLatch callbackReceived = new CountDownLatch(1);
private Future<Long> future;
@Override
public void onCompletion(Long result, Exception exception) {
this.result = result;
this.exception = exception;
callbackReceived.countDown();
}
/**
* Set the {@link Future} associated with the write for which this object is a callback.
* @param future the {@link Future} associated with the write for which this object is a callback.
*/
void setFuture(Future<Long> future) {
this.future = future;
}
/**
* Compares the data obtained from the callback with the data obtained from future.
* @throws InterruptedException
* @throws TimeoutException
*/
void compareWithFuture() throws InterruptedException, TimeoutException {
Long futureResult = null;
Exception futureException = null;
try {
futureResult = future.get(1, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
futureException = e;
}
if (!callbackReceived.await(1, TimeUnit.MILLISECONDS)) {
throw new IllegalStateException("Callback has not been invoked even though future.get() has returned");
} else {
if (futureException == null) {
assertEquals("Future and callback results don't match", futureResult, result);
assertNull("There should have been no exception in the callback", exception);
} else {
assertEquals("Future and callback exceptions don't match", Utils.getRootCause(exception).getMessage(),
Utils.getRootCause(futureException).getMessage());
}
}
}
}