/**
* 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.frontend;
import com.github.ambry.clustermap.ClusterMap;
import com.github.ambry.clustermap.MockClusterMap;
import com.github.ambry.commons.LoggingNotificationSystem;
import com.github.ambry.commons.SSLFactory;
import com.github.ambry.commons.TestSSLUtils;
import com.github.ambry.config.FrontendConfig;
import com.github.ambry.config.SSLConfig;
import com.github.ambry.config.VerifiableProperties;
import com.github.ambry.messageformat.BlobProperties;
import com.github.ambry.rest.NettyClient;
import com.github.ambry.rest.RestServer;
import com.github.ambry.rest.RestServiceException;
import com.github.ambry.rest.RestTestUtils;
import com.github.ambry.rest.RestUtils;
import com.github.ambry.router.ByteRange;
import com.github.ambry.utils.Pair;
import com.github.ambry.utils.TestUtils;
import com.github.ambry.utils.Utils;
import com.github.ambry.utils.UtilsTest;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
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.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;
import io.netty.handler.codec.http.multipart.MemoryFileUpload;
import io.netty.util.ReferenceCountUtil;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.*;
/**
* Integration tests for Ambry frontend.
*/
@RunWith(Parameterized.class)
public class FrontendIntegrationTest {
private static final int PLAINTEXT_SERVER_PORT = 1174;
private static final int SSL_SERVER_PORT = 1175;
private static final ClusterMap CLUSTER_MAP;
private static final VerifiableProperties FRONTEND_VERIFIABLE_PROPS;
private static final VerifiableProperties SSL_CLIENT_VERIFIABLE_PROPS;
private static final FrontendConfig FRONTEND_CONFIG;
static {
try {
CLUSTER_MAP = new MockClusterMap();
File trustStoreFile = File.createTempFile("truststore", ".jks");
trustStoreFile.deleteOnExit();
FRONTEND_VERIFIABLE_PROPS = buildFrontendVProps(trustStoreFile);
SSL_CLIENT_VERIFIABLE_PROPS = TestSSLUtils.createSslProps("", SSLFactory.Mode.CLIENT, trustStoreFile, "client");
FRONTEND_CONFIG = new FrontendConfig(FRONTEND_VERIFIABLE_PROPS);
} catch (IOException | GeneralSecurityException e) {
throw new IllegalStateException(e);
}
}
private static RestServer ambryRestServer = null;
private static NettyClient plaintextNettyClient = null;
private static NettyClient sslNettyClient = null;
private final NettyClient nettyClient;
/**
* Running it many times so that keep-alive bugs are caught.
* We also want to test using both the SSL and plaintext ports.
* @return a list of arrays that represent the constructor arguments for that run of the test.
*/
@Parameterized.Parameters
public static List<Object[]> data() {
List<Object[]> parameters = new ArrayList<>();
for (int i = 0; i < 5; i++) {
parameters.add(new Object[]{false});
parameters.add(new Object[]{true});
}
return parameters;
}
/**
* Sets up an Ambry frontend server.
* @throws Exception
*/
@BeforeClass
public static void setup() throws Exception {
ambryRestServer = new RestServer(FRONTEND_VERIFIABLE_PROPS, CLUSTER_MAP, new LoggingNotificationSystem(),
new SSLFactory(new SSLConfig(FRONTEND_VERIFIABLE_PROPS)));
ambryRestServer.start();
plaintextNettyClient = new NettyClient("localhost", PLAINTEXT_SERVER_PORT, null);
sslNettyClient =
new NettyClient("localhost", SSL_SERVER_PORT, new SSLFactory(new SSLConfig(SSL_CLIENT_VERIFIABLE_PROPS)));
}
/**
* Shuts down the Ambry frontend server.
*/
@AfterClass
public static void teardown() {
if (plaintextNettyClient != null) {
plaintextNettyClient.close();
}
if (sslNettyClient != null) {
sslNettyClient.close();
}
if (ambryRestServer != null) {
ambryRestServer.shutdown();
}
}
/**
* @param useSSL {@code true} if SSL should be tested.
*/
public FrontendIntegrationTest(boolean useSSL) {
nettyClient = useSSL ? sslNettyClient : plaintextNettyClient;
}
/**
* Tests blob POST, GET, HEAD and DELETE operations.
* @throws Exception
*/
@Test
public void postGetHeadDeleteTest() throws Exception {
doPostGetHeadDeleteTest(0, false);
doPostGetHeadDeleteTest(FRONTEND_CONFIG.frontendChunkedGetResponseThresholdInBytes - 1, false);
doPostGetHeadDeleteTest(FRONTEND_CONFIG.frontendChunkedGetResponseThresholdInBytes, false);
doPostGetHeadDeleteTest(FRONTEND_CONFIG.frontendChunkedGetResponseThresholdInBytes * 3, false);
}
/**
* Tests multipart POST and verifies it via GET operations.
* @throws Exception
*/
@Test
public void multipartPostGetHeadTest() throws Exception {
doPostGetHeadDeleteTest(0, true);
doPostGetHeadDeleteTest(1024, true);
}
/*
* Tests health check request
* @throws ExecutionException
* @throws InterruptedException
* @throws IOException
*/
@Test
public void healthCheckRequestTest() throws ExecutionException, InterruptedException, IOException {
FullHttpRequest httpRequest =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/healthCheck", Unpooled.buffer(0));
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
final String expectedResponseBody = "GOOD";
ByteBuffer content = getContent(responseParts, expectedResponseBody.length());
assertEquals("GET content does not match original content", expectedResponseBody, new String(content.array()));
}
// helpers
// general
/**
* Method to easily create a request.
* @param httpMethod the {@link HttpMethod} desired.
* @param uri string representation of the desired URI.
* @param headers any associated headers as a {@link HttpHeaders} object. Can be null.
* @param content the content that accompanies the request. Can be null.
* @return A {@link FullHttpRequest} object that defines the request required by the input.
*/
private FullHttpRequest buildRequest(HttpMethod httpMethod, String uri, HttpHeaders headers, ByteBuffer content) {
ByteBuf contentBuf;
if (content != null) {
contentBuf = Unpooled.wrappedBuffer(content);
} else {
contentBuf = Unpooled.buffer(0);
}
FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, httpMethod, uri, contentBuf);
if (headers != null) {
httpRequest.headers().set(headers);
HttpUtil.setTransferEncodingChunked(httpRequest, true);
}
return httpRequest;
}
/**
* Combines all the parts in {@code contents} into one {@link ByteBuffer}.
* @param contents the content of the response.
* @param expectedContentLength the length of the contents in bytes.
* @return a {@link ByteBuffer} that contains all the data in {@code contents}.
*/
private ByteBuffer getContent(Queue<HttpObject> contents, long expectedContentLength) {
ByteBuffer buffer = ByteBuffer.allocate((int) expectedContentLength);
boolean endMarkerFound = false;
for (HttpObject object : contents) {
assertFalse("There should have been no more data after the end marker was found", endMarkerFound);
HttpContent content = (HttpContent) object;
buffer.put(content.content().nioBuffer());
endMarkerFound = object instanceof LastHttpContent;
ReferenceCountUtil.release(content);
}
assertEquals("Content length did not match expected", expectedContentLength, buffer.position());
assertTrue("End marker was not found", endMarkerFound);
buffer.flip();
return buffer;
}
/**
* Verifies that no content has been sent as part of the response or readable bytes is equivalent to 0
* @param contents the content of the response.
*/
private void assertNoContent(Queue<HttpObject> contents) {
boolean endMarkerFound = false;
for (HttpObject object : contents) {
assertFalse("There should have been no more data after the end marker was found", endMarkerFound);
HttpContent content = (HttpContent) object;
assertEquals("No content expected ", 0, content.content().readableBytes());
endMarkerFound = object instanceof LastHttpContent;
ReferenceCountUtil.release(content);
}
assertTrue("There should have been an end marker", endMarkerFound);
}
/**
* Discards all the content in {@code contents}.
* @param contents the content to discard.
* @param expectedDiscardCount the number of {@link HttpObject}s that are expected to discarded.
*/
private void discardContent(Queue<HttpObject> contents, int expectedDiscardCount) {
assertEquals("Objects that will be discarded differ from expected", expectedDiscardCount, contents.size());
boolean endMarkerFound = false;
for (HttpObject object : contents) {
assertFalse("There should have been no more data after the end marker was found", endMarkerFound);
endMarkerFound = object instanceof LastHttpContent;
ReferenceCountUtil.release(object);
}
assertTrue("There should have been an end marker", endMarkerFound);
}
// BeforeClass helpers
/**
* Builds properties required to start a {@link RestServer} as an Ambry frontend server.
* @param trustStoreFile the trust store file to add certificates to for SSL testing.
* @return a {@link VerifiableProperties} with the parameters for an Ambry frontend server.
*/
private static VerifiableProperties buildFrontendVProps(File trustStoreFile)
throws IOException, GeneralSecurityException {
Properties properties = new Properties();
properties.put("rest.server.blob.storage.service.factory",
"com.github.ambry.frontend.AmbryBlobStorageServiceFactory");
properties.put("rest.server.router.factory", "com.github.ambry.router.InMemoryRouterFactory");
properties.put("netty.server.port", Integer.toString(PLAINTEXT_SERVER_PORT));
properties.put("netty.server.ssl.port", Integer.toString(SSL_SERVER_PORT));
properties.put("netty.server.enable.ssl", "true");
// to test that backpressure does not impede correct operation.
properties.put("netty.server.request.buffer.watermark", "1");
TestSSLUtils.addSSLProperties(properties, "", SSLFactory.Mode.SERVER, trustStoreFile, "frontend");
return new VerifiableProperties(properties);
}
// postGetHeadDeleteTest() and multipartPostGetHeadTest() helpers
/**
* Utility to test blob POST, GET, HEAD and DELETE operations for a specified size
* @param contentSize the size of the blob to be tested
* @param multipartPost {@code true} if multipart POST is desired, {@code false} otherwise.
* @throws Exception
*/
private void doPostGetHeadDeleteTest(int contentSize, boolean multipartPost) throws Exception {
ByteBuffer content = ByteBuffer.wrap(TestUtils.getRandomBytes(contentSize));
String serviceId = "postGetHeadDeleteServiceID";
String contentType = "application/octet-stream";
String ownerId = "postGetHeadDeleteOwnerID";
HttpHeaders headers = new DefaultHttpHeaders();
setAmbryHeadersForPut(headers, 7200, false, serviceId, contentType, ownerId);
String blobId;
byte[] usermetadata = null;
if (multipartPost) {
usermetadata = UtilsTest.getRandomString(32).getBytes();
blobId = multipartPostBlobAndVerify(headers, content, ByteBuffer.wrap(usermetadata));
} else {
headers.add(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key1", "value1");
headers.add(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key2", "value2");
blobId = postBlobAndVerify(headers, content);
}
headers.add(RestUtils.Headers.BLOB_SIZE, content.capacity());
getBlobAndVerify(blobId, null, headers, content);
getHeadAndVerify(blobId, null, headers);
ByteRange range = ByteRange.fromLastNBytes(ThreadLocalRandom.current().nextLong(content.capacity() + 1));
getBlobAndVerify(blobId, range, headers, content);
getHeadAndVerify(blobId, range, headers);
if (contentSize > 0) {
range = ByteRange.fromStartOffset(ThreadLocalRandom.current().nextLong(content.capacity()));
getBlobAndVerify(blobId, range, headers, content);
getHeadAndVerify(blobId, range, headers);
long random1 = ThreadLocalRandom.current().nextLong(content.capacity());
long random2 = ThreadLocalRandom.current().nextLong(content.capacity());
range = ByteRange.fromOffsetRange(Math.min(random1, random2), Math.max(random1, random2));
getBlobAndVerify(blobId, range, headers, content);
getHeadAndVerify(blobId, range, headers);
}
getNotModifiedBlobAndVerify(blobId, false);
getUserMetadataAndVerify(blobId, headers, usermetadata);
getBlobInfoAndVerify(blobId, headers, usermetadata);
deleteBlobAndVerify(blobId);
// check GET, HEAD and DELETE after delete.
verifyOperationsAfterDelete(blobId);
}
/**
* Sets headers that helps build {@link BlobProperties} on the server. See argument list for the headers that are set.
* Any other headers have to be set explicitly.
* @param httpHeaders the {@link HttpHeaders} where the headers should be set.
* @param ttlInSecs sets the {@link RestUtils.Headers#TTL} header. Set to {@link Utils#Infinite_Time} if no
* expiry.
* @param isPrivate sets the {@link RestUtils.Headers#PRIVATE} header. Allowed values: true, false.
* @param serviceId sets the {@link RestUtils.Headers#SERVICE_ID} header. Required.
* @param contentType sets the {@link RestUtils.Headers#AMBRY_CONTENT_TYPE} header. Required and has to be a valid MIME
* type.
* @param ownerId sets the {@link RestUtils.Headers#OWNER_ID} header. Optional - if not required, send null.
* @throws IllegalArgumentException if any of {@code headers}, {@code serviceId}, {@code contentType} is null or if
* {@code contentLength} < 0 or if {@code ttlInSecs} < -1.
*/
private void setAmbryHeadersForPut(HttpHeaders httpHeaders, long ttlInSecs, boolean isPrivate,
String serviceId, String contentType, String ownerId) {
if (httpHeaders != null && ttlInSecs >= -1 && serviceId != null && contentType != null) {
httpHeaders.add(RestUtils.Headers.TTL, ttlInSecs);
httpHeaders.add(RestUtils.Headers.PRIVATE, isPrivate);
httpHeaders.add(RestUtils.Headers.SERVICE_ID, serviceId);
httpHeaders.add(RestUtils.Headers.AMBRY_CONTENT_TYPE, contentType);
if (ownerId != null) {
httpHeaders.add(RestUtils.Headers.OWNER_ID, ownerId);
}
} else {
throw new IllegalArgumentException("Some required arguments are null. Cannot set ambry headers");
}
}
/**
* Posts a blob with the given {@code headers} and {@code content}.
* @param headers the headers required.
* @param content the content of the blob.
* @return the blob ID of the blob.
* @throws ExecutionException
* @throws InterruptedException
*/
private String postBlobAndVerify(HttpHeaders headers, ByteBuffer content)
throws ExecutionException, InterruptedException {
FullHttpRequest httpRequest = buildRequest(HttpMethod.POST, "/", headers, content);
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", HttpResponseStatus.CREATED, response.status());
assertTrue("No Date header", response.headers().getTimeMillis(HttpHeaderNames.DATE, -1) != -1);
assertTrue("No " + RestUtils.Headers.CREATION_TIME,
response.headers().get(RestUtils.Headers.CREATION_TIME, null) != null);
assertEquals("Content-Length is not 0", 0, HttpUtil.getContentLength(response));
String blobId = response.headers().get(HttpHeaderNames.LOCATION, null);
if (blobId == null) {
fail("postBlobAndVerify did not return a blob ID");
}
discardContent(responseParts, 1);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
return blobId;
}
/**
* Gets the blob with blob ID {@code blobId} and verifies that the headers and content match with what is expected.
* @param blobId the blob ID of the blob to GET.
* @param range the {@link ByteRange} for the request.
* @param expectedHeaders the expected headers in the response.
* @param expectedContent the expected content of the blob.
* @throws ExecutionException
* @throws InterruptedException
*/
private void getBlobAndVerify(String blobId, ByteRange range, HttpHeaders expectedHeaders, ByteBuffer expectedContent)
throws ExecutionException, InterruptedException, RestServiceException {
HttpHeaders headers = null;
if (range != null) {
headers = new DefaultHttpHeaders().add(RestUtils.Headers.RANGE, RestTestUtils.getRangeHeaderString(range));
}
FullHttpRequest httpRequest = buildRequest(HttpMethod.GET, blobId, headers, null);
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status",
range == null ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT, response.status());
checkCommonGetHeadHeaders(response.headers());
assertEquals("Content-Type does not match", expectedHeaders.get(RestUtils.Headers.AMBRY_CONTENT_TYPE),
response.headers().get(HttpHeaderNames.CONTENT_TYPE));
assertEquals(RestUtils.Headers.BLOB_SIZE + " does not match", expectedHeaders.get(RestUtils.Headers.BLOB_SIZE),
response.headers().get(RestUtils.Headers.BLOB_SIZE));
assertEquals("Accept-Ranges not set correctly", "bytes", response.headers().get(RestUtils.Headers.ACCEPT_RANGES));
byte[] expectedContentArray = expectedContent.array();
if (range != null) {
long blobSize = Long.parseLong(expectedHeaders.get(RestUtils.Headers.BLOB_SIZE));
assertEquals("Content-Range header not set correctly",
RestUtils.buildContentRangeAndLength(range, blobSize).getFirst(),
response.headers().get(RestUtils.Headers.CONTENT_RANGE));
ByteRange resolvedRange = range.toResolvedByteRange(blobSize);
expectedContentArray = Arrays.copyOfRange(expectedContentArray, (int) resolvedRange.getStartOffset(),
(int) resolvedRange.getEndOffset() + 1);
} else {
assertNull("Content-Range header should not be set", response.headers().get(RestUtils.Headers.CONTENT_RANGE));
}
if (expectedContentArray.length < FRONTEND_CONFIG.frontendChunkedGetResponseThresholdInBytes) {
assertEquals("Content-length not as expected", expectedContentArray.length, HttpUtil.getContentLength(response));
}
verifyCacheHeaders(Boolean.parseBoolean(expectedHeaders.get(RestUtils.Headers.PRIVATE)), response);
byte[] responseContentArray = getContent(responseParts, expectedContentArray.length).array();
assertArrayEquals("GET content does not match original content", expectedContentArray, responseContentArray);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
}
/**
* Gets the blob with blob ID {@code blobId} and verifies that the blob is not returned as blob is not modified
* @param blobId the blob ID of the blob to GET.
* @param isPrivate {@code true} if the blob is private, {@code false} if not.
* @throws Exception
*/
private void getNotModifiedBlobAndVerify(String blobId, boolean isPrivate) throws Exception {
HttpHeaders headers = new DefaultHttpHeaders();
headers.add(RestUtils.Headers.IF_MODIFIED_SINCE, new Date());
FullHttpRequest httpRequest = buildRequest(HttpMethod.GET, blobId, headers, null);
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", HttpResponseStatus.NOT_MODIFIED, response.status());
assertNotNull("Date header should be set", response.headers().get(RestUtils.Headers.DATE));
assertNotNull("Last-Modified header should be set", response.headers().get("Last-Modified"));
assertNull("Content-Length should not be set", response.headers().get(RestUtils.Headers.CONTENT_LENGTH));
assertNull("Accept-Ranges should not be set", response.headers().get(RestUtils.Headers.ACCEPT_RANGES));
assertNull("Content-Range header should not be set", response.headers().get(RestUtils.Headers.CONTENT_RANGE));
assertNull(RestUtils.Headers.BLOB_SIZE + " should have been null ",
response.headers().get(RestUtils.Headers.BLOB_SIZE));
assertNull("Content-Type should have been null", response.headers().get(RestUtils.Headers.CONTENT_TYPE));
verifyCacheHeaders(isPrivate, response);
assertNoContent(responseParts);
}
/**
* Gets the user metadata of the blob with blob ID {@code blobId} and verifies them against what is expected.
* @param blobId the blob ID of the blob to HEAD.
* @param expectedHeaders the expected headers in the response.
* @param usermetadata if non-null, this is expected to come as the body.
* @throws ExecutionException
* @throws InterruptedException
*/
private void getUserMetadataAndVerify(String blobId, HttpHeaders expectedHeaders, byte[] usermetadata)
throws ExecutionException, InterruptedException {
FullHttpRequest httpRequest =
buildRequest(HttpMethod.GET, blobId + "/" + RestUtils.SubResource.UserMetadata, null, null);
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
checkCommonGetHeadHeaders(response.headers());
verifyUserMetadata(expectedHeaders, response, usermetadata, responseParts);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
}
/**
* Gets the blob info of the blob with blob ID {@code blobId} and verifies them against what is expected.
* @param blobId the blob ID of the blob to HEAD.
* @param expectedHeaders the expected headers in the response.
* @param usermetadata if non-null, this is expected to come as the body.
* @throws ExecutionException
* @throws InterruptedException
*/
private void getBlobInfoAndVerify(String blobId, HttpHeaders expectedHeaders, byte[] usermetadata)
throws ExecutionException, InterruptedException {
FullHttpRequest httpRequest =
buildRequest(HttpMethod.GET, blobId + "/" + RestUtils.SubResource.BlobInfo, null, null);
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", HttpResponseStatus.OK, response.status());
checkCommonGetHeadHeaders(response.headers());
verifyBlobProperties(expectedHeaders, response);
verifyUserMetadata(expectedHeaders, response, usermetadata, responseParts);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
}
/**
* Gets the headers of the blob with blob ID {@code blobId} and verifies them against what is expected.
* @param blobId the blob ID of the blob to HEAD.
* @param range the {@link ByteRange} for the request.
* @param expectedHeaders the expected headers in the response.
* @throws ExecutionException
* @throws InterruptedException
*/
private void getHeadAndVerify(String blobId, ByteRange range, HttpHeaders expectedHeaders)
throws ExecutionException, InterruptedException, RestServiceException {
HttpHeaders headers = null;
if (range != null) {
headers = new DefaultHttpHeaders().add(RestUtils.Headers.RANGE, RestTestUtils.getRangeHeaderString(range));
}
FullHttpRequest httpRequest = buildRequest(HttpMethod.HEAD, blobId, headers, null);
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status",
range == null ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT, response.status());
checkCommonGetHeadHeaders(response.headers());
long contentLength = Long.parseLong(expectedHeaders.get(RestUtils.Headers.BLOB_SIZE));
if (range != null) {
Pair<String, Long> rangeAndLength = RestUtils.buildContentRangeAndLength(range, contentLength);
assertEquals("Content-Range header not set correctly", rangeAndLength.getFirst(),
response.headers().get(RestUtils.Headers.CONTENT_RANGE));
contentLength = rangeAndLength.getSecond();
} else {
assertNull("Content-Range header should not be set", response.headers().get(RestUtils.Headers.CONTENT_RANGE));
}
assertEquals("Accept-Ranges not set correctly", "bytes", response.headers().get(RestUtils.Headers.ACCEPT_RANGES));
assertEquals(RestUtils.Headers.CONTENT_LENGTH + " does not match expected", contentLength,
HttpUtil.getContentLength(response));
assertEquals(RestUtils.Headers.CONTENT_TYPE + " does not match " + RestUtils.Headers.AMBRY_CONTENT_TYPE,
expectedHeaders.get(RestUtils.Headers.AMBRY_CONTENT_TYPE),
response.headers().get(HttpHeaderNames.CONTENT_TYPE));
verifyBlobProperties(expectedHeaders, response);
discardContent(responseParts, 1);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
}
/**
* Verifies blob properties from output, to that sent in during input
* @param expectedHeaders the expected headers in the response.
* @param response the {@link HttpResponse} that contains the headers.
*/
private void verifyBlobProperties(HttpHeaders expectedHeaders, HttpResponse response) {
assertEquals("Blob size does not match", Long.parseLong(expectedHeaders.get(RestUtils.Headers.BLOB_SIZE)),
Long.parseLong(response.headers().get(RestUtils.Headers.BLOB_SIZE)));
assertEquals(RestUtils.Headers.SERVICE_ID + " does not match", expectedHeaders.get(RestUtils.Headers.SERVICE_ID),
response.headers().get(RestUtils.Headers.SERVICE_ID));
assertEquals(RestUtils.Headers.PRIVATE + " does not match", expectedHeaders.get(RestUtils.Headers.PRIVATE),
response.headers().get(RestUtils.Headers.PRIVATE));
assertEquals(RestUtils.Headers.AMBRY_CONTENT_TYPE + " does not match",
expectedHeaders.get(RestUtils.Headers.AMBRY_CONTENT_TYPE),
response.headers().get(RestUtils.Headers.AMBRY_CONTENT_TYPE));
assertTrue("No " + RestUtils.Headers.CREATION_TIME,
response.headers().get(RestUtils.Headers.CREATION_TIME, null) != null);
if (Long.parseLong(expectedHeaders.get(RestUtils.Headers.TTL)) != Utils.Infinite_Time) {
assertEquals(RestUtils.Headers.TTL + " does not match", expectedHeaders.get(RestUtils.Headers.TTL),
response.headers().get(RestUtils.Headers.TTL));
}
if (expectedHeaders.contains(RestUtils.Headers.OWNER_ID)) {
assertEquals(RestUtils.Headers.OWNER_ID + " does not match", expectedHeaders.get(RestUtils.Headers.OWNER_ID),
response.headers().get(RestUtils.Headers.OWNER_ID));
}
}
/**
* Verifies User metadata headers from output, to that sent in during input
* @param expectedHeaders the expected headers in the response.
* @param response the {@link HttpResponse} which contains the headers of the response.
* @param usermetadata if non-null, this is expected to come as the body.
* @param content the content accompanying the response.
*/
private void verifyUserMetadata(HttpHeaders expectedHeaders, HttpResponse response, byte[] usermetadata,
Queue<HttpObject> content) {
if (usermetadata == null) {
assertEquals("Content-Length is not 0", 0, HttpUtil.getContentLength(response));
for (Map.Entry<String, String> header : expectedHeaders) {
String key = header.getKey();
if (key.startsWith(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX)) {
assertEquals("Value for " + key + " does not match in user metadata", header.getValue(),
response.headers().get(key));
}
}
for (Map.Entry<String, String> header : response.headers()) {
String key = header.getKey();
if (key.startsWith(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX)) {
assertTrue("Key " + key + " does not exist in expected headers", expectedHeaders.contains(key));
}
}
discardContent(content, 1);
} else {
assertEquals("Content-Length is not as expected", usermetadata.length, HttpUtil.getContentLength(response));
byte[] receivedMetadata = getContent(content, HttpUtil.getContentLength(response)).array();
assertArrayEquals("User metadata does not match original", usermetadata, receivedMetadata);
}
}
/**
* Deletes the blob with blob ID {@code blobId} and verifies the response returned.
* @param blobId the blob ID of the blob to DELETE.
* @throws ExecutionException
* @throws InterruptedException
*/
private void deleteBlobAndVerify(String blobId) throws ExecutionException, InterruptedException {
FullHttpRequest httpRequest = buildRequest(HttpMethod.DELETE, blobId, null, null);
verifyDeleted(httpRequest, HttpResponseStatus.ACCEPTED);
}
/**
* Verifies that the right response code is returned for GET, HEAD and DELETE once a blob is deleted.
* @param blobId the ID of the blob that was deleted.
* @throws ExecutionException
* @throws InterruptedException
*/
private void verifyOperationsAfterDelete(String blobId) throws ExecutionException, InterruptedException {
FullHttpRequest httpRequest = buildRequest(HttpMethod.GET, blobId, null, null);
verifyDeleted(httpRequest, HttpResponseStatus.GONE);
httpRequest = buildRequest(HttpMethod.HEAD, blobId, null, null);
verifyDeleted(httpRequest, HttpResponseStatus.GONE);
httpRequest = buildRequest(HttpMethod.DELETE, blobId, null, null);
verifyDeleted(httpRequest, HttpResponseStatus.ACCEPTED);
}
/**
* Verifies that a request returns the right response code once the blob has been deleted.
* @param httpRequest the {@link FullHttpRequest} to send to the server.
* @param expectedStatusCode the expected {@link HttpResponseStatus}.
* @throws ExecutionException
* @throws InterruptedException
*/
private void verifyDeleted(FullHttpRequest httpRequest, HttpResponseStatus expectedStatusCode)
throws ExecutionException, InterruptedException {
Queue<HttpObject> responseParts = nettyClient.sendRequest(httpRequest, null, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", expectedStatusCode, response.status());
assertTrue("No Date header", response.headers().get(HttpHeaderNames.DATE, null) != null);
discardContent(responseParts, 1);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
}
/**
* Checks headers that are common to HEAD and GET.
* @param receivedHeaders the {@link HttpHeaders} that were received.
*/
private void checkCommonGetHeadHeaders(HttpHeaders receivedHeaders) {
assertTrue("No Date header", receivedHeaders.get(HttpHeaderNames.DATE) != null);
assertTrue("No Last-Modified header", receivedHeaders.get(HttpHeaderNames.LAST_MODIFIED) != null);
}
/**
* Verifies that the right cache headers are returned.
* @param isPrivate {@code true} if the blob is private, {@code false} if not.
* @param response the {@link HttpResponse}.
*/
private void verifyCacheHeaders(boolean isPrivate, HttpResponse response) {
if (isPrivate) {
Assert.assertEquals("Cache-Control value not as expected", "private, no-cache, no-store, proxy-revalidate",
response.headers().get(RestUtils.Headers.CACHE_CONTROL));
Assert.assertEquals("Pragma value not as expected", "no-cache", response.headers().get(RestUtils.Headers.PRAGMA));
} else {
String expiresValue = response.headers().get(RestUtils.Headers.EXPIRES);
assertNotNull("Expires value should be non null", expiresValue);
assertTrue("Expires value should be in future",
RestUtils.getTimeFromDateString(expiresValue) > System.currentTimeMillis());
Assert.assertEquals("Cache-Control value not as expected",
"max-age=" + FRONTEND_CONFIG.frontendCacheValiditySeconds,
response.headers().get(RestUtils.Headers.CACHE_CONTROL));
Assert.assertNull("Pragma value should not have been set", response.headers().get(RestUtils.Headers.PRAGMA));
}
}
/**
* Posts a blob with the given {@code headers} and {@code content}.
* @param headers the headers required.
* @param content the content of the blob.
* @param usermetadata the {@link ByteBuffer} that represents user metadata
* @return the blob ID of the blob.
* @throws Exception
*/
private String multipartPostBlobAndVerify(HttpHeaders headers, ByteBuffer content, ByteBuffer usermetadata)
throws Exception {
HttpRequest httpRequest = RestTestUtils.createRequest(HttpMethod.POST, "/", headers);
HttpPostRequestEncoder encoder = createEncoder(httpRequest, content, usermetadata);
Queue<HttpObject> responseParts = nettyClient.sendRequest(encoder.finalizeRequest(), encoder, null).get();
HttpResponse response = (HttpResponse) responseParts.poll();
assertEquals("Unexpected response status", HttpResponseStatus.CREATED, response.status());
assertTrue("No Date header", response.headers().get(HttpHeaderNames.DATE, null) != null);
assertTrue("No " + RestUtils.Headers.CREATION_TIME,
response.headers().get(RestUtils.Headers.CREATION_TIME, null) != null);
assertEquals("Content-Length is not 0", 0, HttpUtil.getContentLength(response));
String blobId = response.headers().get(HttpHeaderNames.LOCATION, null);
if (blobId == null) {
fail("postBlobAndVerify did not return a blob ID");
}
discardContent(responseParts, 1);
assertTrue("Channel should be active", HttpUtil.isKeepAlive(response));
return blobId;
}
/**
* Creates a {@link HttpPostRequestEncoder} that encodes the given {@code request} and {@code blobContent}.
* @param request the {@link HttpRequest} containing headers and other metadata about the request.
* @param blobContent the {@link ByteBuffer} that represents the content of the blob.
* @param usermetadata the {@link ByteBuffer} that represents user metadata
* @return a {@link HttpPostRequestEncoder} that can encode the {@code request} and {@code blobContent}.
* @throws HttpPostRequestEncoder.ErrorDataEncoderException
* @throws IOException
*/
private HttpPostRequestEncoder createEncoder(HttpRequest request, ByteBuffer blobContent, ByteBuffer usermetadata)
throws HttpPostRequestEncoder.ErrorDataEncoderException, IOException {
HttpDataFactory httpDataFactory = new DefaultHttpDataFactory(false);
HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(httpDataFactory, request, true);
FileUpload fileUpload = new MemoryFileUpload(RestUtils.MultipartPost.BLOB_PART, RestUtils.MultipartPost.BLOB_PART,
"application/octet-stream", "", Charset.forName("UTF-8"), blobContent.remaining());
fileUpload.setContent(Unpooled.wrappedBuffer(blobContent));
encoder.addBodyHttpData(fileUpload);
fileUpload =
new MemoryFileUpload(RestUtils.MultipartPost.USER_METADATA_PART, RestUtils.MultipartPost.USER_METADATA_PART,
"application/octet-stream", "", Charset.forName("UTF-8"), usermetadata.remaining());
fileUpload.setContent(Unpooled.wrappedBuffer(usermetadata));
encoder.addBodyHttpData(fileUpload);
return encoder;
}
}