/**
* 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.github.ambry.messageformat.BlobProperties;
import com.github.ambry.protocol.GetOption;
import com.github.ambry.router.ByteRange;
import com.github.ambry.router.GetBlobOptions;
import com.github.ambry.utils.Crc32;
import com.github.ambry.utils.Pair;
import com.github.ambry.utils.Utils;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.TimeZone;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Unit tests for {@link RestUtils}.
*/
public class RestUtilsTest {
private static final Random RANDOM = new Random();
private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz";
/**
* Tests building of {@link BlobProperties} given good input (all arguments in the number and format expected).
* @throws Exception
*/
@Test
public void getBlobPropertiesGoodInputTest() throws Exception {
JSONObject headers = new JSONObject();
setAmbryHeadersForPut(headers, Long.toString(RANDOM.nextInt(10000)), Boolean.toString(RANDOM.nextBoolean()),
generateRandomString(10), "image/gif", generateRandomString(10));
verifyBlobPropertiesConstructionSuccess(headers);
}
/**
* Tests building of {@link BlobProperties} given varied input. Some input fail (tests check the correct error code),
* some should succeed (check for default values expected).
* @throws Exception
*/
@Test
public void getBlobPropertiesVariedInputTest() throws Exception {
String ttl = Long.toString(RANDOM.nextInt(10000));
String isPrivate = Boolean.toString(RANDOM.nextBoolean());
String serviceId = generateRandomString(10);
String contentType = "image/gif";
String ownerId = generateRandomString(10);
JSONObject headers;
// failure required.
// ttl not a number.
headers = new JSONObject();
setAmbryHeadersForPut(headers, "NaN", isPrivate, serviceId, contentType, ownerId);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.InvalidArgs);
// ttl < -1.
headers = new JSONObject();
setAmbryHeadersForPut(headers, "-2", isPrivate, serviceId, contentType, ownerId);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.InvalidArgs);
// isPrivate not true or false.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, "!(true||false)", serviceId, contentType, ownerId);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.InvalidArgs);
// serviceId missing.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, null, contentType, ownerId);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.MissingArgs);
// serviceId null.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, null, contentType, ownerId);
headers.put(RestUtils.Headers.SERVICE_ID, JSONObject.NULL);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.InvalidArgs);
// contentType missing.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, null, ownerId);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.MissingArgs);
// contentType null.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, null, ownerId);
headers.put(RestUtils.Headers.AMBRY_CONTENT_TYPE, JSONObject.NULL);
verifyBlobPropertiesConstructionFailure(headers, RestServiceErrorCode.InvalidArgs);
// too many values for some headers.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, contentType, ownerId);
tooManyValuesTest(headers, RestUtils.Headers.TTL);
tooManyValuesTest(headers, RestUtils.Headers.PRIVATE);
// no failures.
// ttl missing. Should be infinite time by default.
// isPrivate missing. Should be false by default.
// ownerId missing.
headers = new JSONObject();
setAmbryHeadersForPut(headers, null, null, serviceId, contentType, null);
verifyBlobPropertiesConstructionSuccess(headers);
// ttl null.
headers = new JSONObject();
setAmbryHeadersForPut(headers, null, isPrivate, serviceId, contentType, ownerId);
headers.put(RestUtils.Headers.TTL, JSONObject.NULL);
verifyBlobPropertiesConstructionSuccess(headers);
// isPrivate null.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, contentType, ownerId);
headers.put(RestUtils.Headers.PRIVATE, JSONObject.NULL);
verifyBlobPropertiesConstructionSuccess(headers);
// ownerId null.
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, contentType, null);
headers.put(RestUtils.Headers.OWNER_ID, JSONObject.NULL);
verifyBlobPropertiesConstructionSuccess(headers);
// blobSize null (should be ignored)
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, contentType, null);
headers.put(RestUtils.Headers.BLOB_SIZE, JSONObject.NULL);
verifyBlobPropertiesConstructionSuccess(headers);
// blobSize negative (should succeed)
headers = new JSONObject();
setAmbryHeadersForPut(headers, ttl, isPrivate, serviceId, contentType, null);
headers.put(RestUtils.Headers.BLOB_SIZE, -1);
verifyBlobPropertiesConstructionSuccess(headers);
}
/**
* Tests building of user metadata.
* @throws Exception
*/
@Test
public void getUserMetadataTest() throws Exception {
byte[] usermetadata = RestUtils.buildUsermetadata(new HashMap<String, Object>());
assertArrayEquals("Unexpected user metadata", new byte[0], usermetadata);
}
/**
* Tests building of User Metadata with good input
* @throws Exception
*/
@Test
public void getUserMetadataGoodInputTest() throws Exception {
JSONObject headers = new JSONObject();
setAmbryHeadersForPut(headers, Long.toString(RANDOM.nextInt(10000)), Boolean.toString(RANDOM.nextBoolean()),
generateRandomString(10), "image/gif", generateRandomString(10));
Map<String, String> userMetadata = new HashMap<String, String>();
userMetadata.put(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key1", "value1");
userMetadata.put(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key2", "value2");
setUserMetadataHeaders(headers, userMetadata);
verifyUserMetadataConstructionSuccess(headers, userMetadata);
}
/**
* Tests building of User Metadata when the {@link RestRequest} contains an arg with name
* {@link RestUtils.MultipartPost#USER_METADATA_PART}.
* @throws Exception
*/
@Test
public void getUserMetadataWithUserMetadataArgTest() throws Exception {
byte[] original = new byte[100];
RANDOM.nextBytes(original);
JSONObject headers = new JSONObject();
headers.put(RestUtils.MultipartPost.USER_METADATA_PART, ByteBuffer.wrap(original));
RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers);
byte[] rcvd = RestUtils.buildUsermetadata(restRequest.getArgs());
assertArrayEquals("Received user metadata does not match with original", original, rcvd);
}
/**
* Tests building of User Metadata with unusual input
* @throws Exception
*/
@Test
public void getUserMetadataUnusualInputTest() throws Exception {
JSONObject headers = new JSONObject();
setAmbryHeadersForPut(headers, Long.toString(RANDOM.nextInt(10000)), Boolean.toString(RANDOM.nextBoolean()),
generateRandomString(10), "image/gif", generateRandomString(10));
Map<String, String> userMetadata = new HashMap<String, String>();
String key1 = RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key1";
userMetadata.put(key1, "value1");
// no valid prefix
userMetadata.put("key2", "value2_1");
// valid prefix as suffix
userMetadata.put("key3" + RestUtils.Headers.USER_META_DATA_HEADER_PREFIX, "value3");
// empty value
userMetadata.put(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key4", "");
setUserMetadataHeaders(headers, userMetadata);
RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers);
byte[] userMetadataByteArray = RestUtils.buildUsermetadata(restRequest.getArgs());
Map<String, String> userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
// key1, output should be same as input
String key = RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key1";
assertTrue(key + " not found in user metadata map ", userMetadataMap.containsKey(key));
assertEquals("Value for " + key + " didnt match input value ", userMetadata.get(key), userMetadataMap.get(key));
// key4 should match
key = RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + "key4";
assertTrue(key + " not found in user metadata map ", userMetadataMap.containsKey(key));
assertEquals("Value for " + key + " didnt match input value ", userMetadata.get(key), userMetadataMap.get(key));
assertEquals("Size of map unexpected ", 2, userMetadataMap.size());
}
/**
* Tests building of User Metadata with empty input
* @throws Exception
*/
@Test
public void getEmptyUserMetadataInputTest() throws Exception {
JSONObject headers = new JSONObject();
setAmbryHeadersForPut(headers, Long.toString(RANDOM.nextInt(10000)), Boolean.toString(RANDOM.nextBoolean()),
generateRandomString(10), "image/gif", generateRandomString(10));
Map<String, String> userMetadata = new HashMap<String, String>();
setUserMetadataHeaders(headers, userMetadata);
RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers);
byte[] userMetadataByteArray = RestUtils.buildUsermetadata(restRequest.getArgs());
Map<String, String> userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
}
/**
* Tests deserializing user metadata from byte array
* @throws Exception
*/
@Test
public void getUserMetadataFromByteArrayComplexTest() throws Exception {
Map<String, String> userMetadataMap = null;
// user metadata of size 1 byte
byte[] userMetadataByteArray = new byte[1];
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// user metadata with just the version
userMetadataByteArray = new byte[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// user metadata with wrong version
userMetadataByteArray = new byte[4];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 3);
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// 0 sized user metadata
userMetadataByteArray = new byte[12];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(4);
byteBuffer.putInt(0);
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// wrong size
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
String key = "key1";
byte[] keyInBytes = key.getBytes(StandardCharsets.US_ASCII);
int keyLength = keyInBytes.length;
byteBuffer.putInt(21);
byteBuffer.putInt(1);
byteBuffer.putInt(keyLength);
byteBuffer.put(keyInBytes);
String value = "value1";
byte[] valueInBytes = value.getBytes(StandardCharsets.US_ASCII);
int valueLength = valueInBytes.length;
byteBuffer.putInt(valueLength);
byteBuffer.put(valueInBytes);
Crc32 crc32 = new Crc32();
crc32.update(userMetadataByteArray, 0, userMetadataByteArray.length - 8);
byteBuffer.putLong(crc32.getValue());
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// wrong total number of entries
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(22);
byteBuffer.putInt(2);
byteBuffer.putInt(keyLength);
byteBuffer.put(keyInBytes);
byteBuffer.putInt(valueLength);
byteBuffer.put(valueInBytes);
crc32 = new Crc32();
crc32.update(userMetadataByteArray, 0, userMetadataByteArray.length - 8);
byteBuffer.putLong(crc32.getValue());
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// diff key length
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(22);
byteBuffer.putInt(1);
byteBuffer.putInt(keyLength + 1);
byteBuffer.put(keyInBytes);
byteBuffer.putInt(valueLength);
byteBuffer.put(valueInBytes);
crc32 = new Crc32();
crc32.update(userMetadataByteArray, 0, userMetadataByteArray.length - 8);
byteBuffer.putLong(crc32.getValue());
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// diff value length
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(22);
byteBuffer.putInt(1);
byteBuffer.putInt(keyLength);
byteBuffer.put(keyInBytes);
byteBuffer.putInt(valueLength + 1);
byteBuffer.put(valueInBytes);
crc32 = new Crc32();
crc32.update(userMetadataByteArray, 0, userMetadataByteArray.length - 8);
byteBuffer.putLong(crc32.getValue());
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// no crc
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(22);
byteBuffer.putInt(1);
byteBuffer.putInt(keyLength);
byteBuffer.put(keyInBytes);
byteBuffer.putInt(valueLength);
byteBuffer.put(valueInBytes);
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// wrong crc
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(22);
byteBuffer.putInt(1);
byteBuffer.putInt(keyLength);
byteBuffer.put(keyInBytes);
byteBuffer.putInt(valueLength);
byteBuffer.put(valueInBytes);
crc32 = new Crc32();
crc32.update(userMetadataByteArray, 0, userMetadataByteArray.length - 8);
byteBuffer.putLong(crc32.getValue() - 1);
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertNull("UserMetadata should have been null ", userMetadataMap);
// correct crc
userMetadataByteArray = new byte[36];
byteBuffer = ByteBuffer.wrap(userMetadataByteArray);
byteBuffer.putShort((short) 1);
byteBuffer.putInt(22);
byteBuffer.putInt(1);
byteBuffer.putInt(keyLength);
byteBuffer.put(keyInBytes);
byteBuffer.putInt(valueLength);
byteBuffer.put(valueInBytes);
crc32 = new Crc32();
crc32.update(userMetadataByteArray, 0, userMetadataByteArray.length - 8);
byteBuffer.putLong(crc32.getValue());
userMetadataMap = RestUtils.buildUserMetadata(userMetadataByteArray);
assertEquals("Sizes don't match ", userMetadataMap.size(), 1);
assertTrue("User metadata " + RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + key + " not found in user metadata ",
userMetadataMap.containsKey(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + key));
assertEquals("User metadata " + RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + key + " value don't match ", value,
userMetadataMap.get(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX + key));
}
/**
* Tests {@link RestUtils#getOperationOrBlobIdFromUri(RestRequest, RestUtils.SubResource, List)}.
* @throws JSONException
* @throws UnsupportedEncodingException
* @throws URISyntaxException
*/
@Test
public void getOperationOrBlobIdFromUriTest() throws JSONException, UnsupportedEncodingException, URISyntaxException {
String baseId = "expectedOp";
String queryString = "?queryParam1=queryValue1&queryParam2=queryParam2=queryValue2";
String[] validIdUris = {"/" + baseId, "/" + baseId + "/random/extra", baseId, baseId + "/random/extra"};
List<String> prefixesToTestOn = Arrays.asList("", "/media", "/toRemove", "/orNotToRemove");
List<String> prefixesToRemove = Arrays.asList("/media", "/toRemove");
// construct test cases
Map<String, String> testCases = new HashMap<>();
for (String validIdUri : validIdUris) {
// the uri as is (e.g. "/expectedOp).
testCases.put(validIdUri, validIdUri);
// the uri with a query string (e.g. "/expectedOp?param=value").
testCases.put(validIdUri + queryString, validIdUri);
for (RestUtils.SubResource subResource : RestUtils.SubResource.values()) {
String subResourceStr = "/" + subResource.name();
// the uri with a sub-resource (e.g. "/expectedOp/BlobInfo").
testCases.put(validIdUri + subResourceStr, validIdUri);
// the uri with a sub-resource and query string (e.g. "/expectedOp/BlobInfo?param=value").
testCases.put(validIdUri + subResourceStr + queryString, validIdUri);
}
}
// test each case on each prefix.
for (String prefixToTestOn : prefixesToTestOn) {
for (Map.Entry<String, String> testCase : testCases.entrySet()) {
String testPath = testCase.getKey();
// skip the ones with no leading slash if prefix is not "". Otherwise they become -> "/prefixexpectedOp".
if (prefixToTestOn.isEmpty() || testPath.startsWith("/")) {
String realTestPath = prefixToTestOn + testPath;
String expectedOutput = testCase.getValue();
expectedOutput = prefixesToRemove.contains(prefixToTestOn) ? expectedOutput : prefixToTestOn + expectedOutput;
RestRequest restRequest = createRestRequest(RestMethod.GET, realTestPath, null);
assertEquals("Unexpected operation/blob id for: " + realTestPath, expectedOutput,
RestUtils.getOperationOrBlobIdFromUri(restRequest, RestUtils.getBlobSubResource(restRequest),
prefixesToRemove));
}
}
}
}
/**
* Tests {@link RestUtils#getBlobSubResource(RestRequest)}.
* @throws JSONException
* @throws UnsupportedEncodingException
* @throws URISyntaxException
*/
@Test
public void getBlobSubResourceTest() throws JSONException, UnsupportedEncodingException, URISyntaxException {
// sub resource null
String queryString = "?queryParam1=queryValue1&queryParam2=queryParam2=queryValue2";
String[] nullUris = {"/op", "/op/", "/op/invalid", "/op/invalid/", "op", "op/", "op/invalid", "op/invalid/"};
for (String uri : nullUris) {
RestRequest restRequest = createRestRequest(RestMethod.GET, uri, null);
assertNull("There was no sub-resource expected in: " + uri, RestUtils.getBlobSubResource(restRequest));
// add a sub-resource at the end as part of the query string.
for (RestUtils.SubResource subResource : RestUtils.SubResource.values()) {
String fullUri = uri + queryString + subResource;
restRequest = createRestRequest(RestMethod.GET, fullUri, null);
assertNull("There was no sub-resource expected in: " + fullUri, RestUtils.getBlobSubResource(restRequest));
}
}
// valid sub resource
String[] nonNullUris = {"/op/", "/op/random/", "op/", "op/random/"};
for (String uri : nonNullUris) {
for (RestUtils.SubResource subResource : RestUtils.SubResource.values()) {
String fullUri = uri + subResource;
RestRequest restRequest = createRestRequest(RestMethod.GET, fullUri, null);
assertEquals("Unexpected sub resource in uri: " + fullUri, subResource,
RestUtils.getBlobSubResource(restRequest));
// add a query-string.
fullUri = uri + subResource + queryString;
restRequest = createRestRequest(RestMethod.GET, fullUri, null);
assertEquals("Unexpected sub resource in uri: " + fullUri, subResource,
RestUtils.getBlobSubResource(restRequest));
}
}
}
/**
* Tests {@link RestUtils#toSecondsPrecisionInMs(long)}.
*/
@Test
public void toSecondsPrecisionInMsTest() {
assertEquals(0, RestUtils.toSecondsPrecisionInMs(999));
assertEquals(1000, RestUtils.toSecondsPrecisionInMs(1000));
assertEquals(1000, RestUtils.toSecondsPrecisionInMs(1001));
}
/**
* Tests {@link RestUtils#getTimeFromDateString(String)}.
*/
@Test
public void getTimeFromDateStringTest() {
SimpleDateFormat dateFormatter = new SimpleDateFormat(RestUtils.HTTP_DATE_FORMAT, Locale.ENGLISH);
dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
long curTime = System.currentTimeMillis();
Date curDate = new Date(curTime);
String dateStr = dateFormatter.format(curDate);
long epochTime = RestUtils.getTimeFromDateString(dateStr);
long actualExpectedTime = (curTime / 1000L) * 1000;
// Note http time is kept in Seconds so last three digits will be 000
assertEquals("Time mismatch ", actualExpectedTime, epochTime);
dateFormatter = new SimpleDateFormat(RestUtils.HTTP_DATE_FORMAT, Locale.CHINA);
curTime = System.currentTimeMillis();
curDate = new Date(curTime);
dateStr = dateFormatter.format(curDate);
// any other locale is not accepted
assertEquals("Should have returned null", null, RestUtils.getTimeFromDateString(dateStr));
assertEquals("Should have returned null", null, RestUtils.getTimeFromDateString("abc"));
}
/**
* This tests the construction of {@link GetBlobOptions} objects with various range and sub-resource settings using
* {@link RestUtils#buildGetBlobOptions(Map, RestUtils.SubResource, GetOption)} and
* {@link RestUtils#buildByteRange(String)}.
* @throws RestServiceException
*/
@Test
public void buildGetBlobOptionsTest() throws RestServiceException {
// no range
doBuildGetBlobOptionsTest(null, null, true, true);
// valid ranges
doBuildGetBlobOptionsTest("bytes=0-7", ByteRange.fromOffsetRange(0, 7), true, false);
doBuildGetBlobOptionsTest("bytes=234-56679090", ByteRange.fromOffsetRange(234, 56679090), true, false);
doBuildGetBlobOptionsTest("bytes=1-", ByteRange.fromStartOffset(1), true, false);
doBuildGetBlobOptionsTest("bytes=12345678-", ByteRange.fromStartOffset(12345678), true, false);
doBuildGetBlobOptionsTest("bytes=-8", ByteRange.fromLastNBytes(8), true, false);
doBuildGetBlobOptionsTest("bytes=-123456789", ByteRange.fromLastNBytes(123456789), true, false);
// bad ranges
String[] badRanges =
{"bytes=0-abcd", "bytes=0as23-44444444", "bytes=22-7777777777777777777777777777777777777777777", "bytes=22--53", "bytes=223-34", "bytes=-34ab", "bytes=--12", "bytes=-12-", "bytes=12ab-", "bytes=---", "btes=3-5", "bytes=345", "bytes=3.14-22", "bytes=3-6.2", "bytes=", "bytes=-", "bytes= -"};
for (String badRange : badRanges) {
doBuildGetBlobOptionsTest(badRange, null, false, false);
}
}
/**
* Test {@link RestUtils#buildContentRangeAndLength(ByteRange, long)}.
*/
@Test
public void buildContentRangeAndLengthTest() throws RestServiceException {
// good cases
doBuildContentRangeAndLengthTest(ByteRange.fromOffsetRange(4, 8), 12, "bytes 4-8/12", 5, true);
doBuildContentRangeAndLengthTest(ByteRange.fromStartOffset(14), 17, "bytes 14-16/17", 3, true);
doBuildContentRangeAndLengthTest(ByteRange.fromLastNBytes(12), 17, "bytes 5-16/17", 12, true);
doBuildContentRangeAndLengthTest(ByteRange.fromLastNBytes(17), 17, "bytes 0-16/17", 17, true);
// bad cases
doBuildContentRangeAndLengthTest(ByteRange.fromOffsetRange(4, 12), 12, null, -1, false);
doBuildContentRangeAndLengthTest(ByteRange.fromOffsetRange(4, 15), 12, null, -1, false);
doBuildContentRangeAndLengthTest(ByteRange.fromStartOffset(12), 12, null, -1, false);
doBuildContentRangeAndLengthTest(ByteRange.fromStartOffset(15), 12, null, -1, false);
doBuildContentRangeAndLengthTest(ByteRange.fromLastNBytes(13), 12, null, -1, false);
}
/**
* Tests {@link RestUtils#getGetOption(RestRequest)}.
* @throws Exception
*/
@Test
public void getGetOptionTest() throws Exception {
for (GetOption option : GetOption.values()) {
JSONObject headers = new JSONObject();
headers.put(RestUtils.Headers.GET_OPTION, option.toString().toLowerCase());
RestRequest restRequest = createRestRequest(RestMethod.GET, "/", headers);
assertEquals("Option returned not as expected", option, RestUtils.getGetOption(restRequest));
}
// no value defined
RestRequest restRequest = createRestRequest(RestMethod.GET, "/", null);
assertEquals("Option returned not as expected", GetOption.None, RestUtils.getGetOption(restRequest));
// bad value
JSONObject headers = new JSONObject();
headers.put(RestUtils.Headers.GET_OPTION, "non_existent_option");
restRequest = createRestRequest(RestMethod.GET, "/", headers);
try {
RestUtils.getGetOption(restRequest);
fail("Should have failed to get GetOption because value of header is invalid");
} catch (RestServiceException e) {
assertEquals("Unexpected RestServiceErrorCode", RestServiceErrorCode.InvalidArgs, e.getErrorCode());
}
}
/**
* Tests {@link RestUtils#getServiceId(RestRequest)}
* @throws Exception
*/
@Test
public void getServiceIdTest() throws Exception {
String serviceId = "the-service-id";
JSONObject headers = new JSONObject();
headers.put(RestUtils.Headers.SERVICE_ID, serviceId);
RestRequest restRequest = createRestRequest(RestMethod.DELETE, "/", headers);
assertEquals("Unexpected service id", serviceId, RestUtils.getServiceId(restRequest));
restRequest = createRestRequest(RestMethod.DELETE, "/", new JSONObject());
assertNull("Should not have found service ID", RestUtils.getServiceId(restRequest));
}
// helpers.
// general.
/**
* Method to easily create {@link RestRequest} objects containing a specific request.
* @param restMethod the {@link RestMethod} desired.
* @param uri string representation of the desired URI.
* @param headers any associated headers as a {@link org.json.JSONObject}.
* @return A {@link RestRequest} object that defines the request required by the input.
* @throws JSONException
* @throws UnsupportedEncodingException
* @throws URISyntaxException
*/
private RestRequest createRestRequest(RestMethod restMethod, String uri, JSONObject headers)
throws JSONException, UnsupportedEncodingException, URISyntaxException {
JSONObject request = new JSONObject();
request.put(MockRestRequest.REST_METHOD_KEY, restMethod);
request.put(MockRestRequest.URI_KEY, uri);
if (headers != null) {
request.put(MockRestRequest.HEADERS_KEY, headers);
}
return new MockRestRequest(request, null);
}
/**
* Generates a string of size {@code length} with random characters from {@link #ALPHABET}.
* @param length the length of random string required.
* @return a string of size {@code length} with random characters from {@link #ALPHABET}.
*/
private String generateRandomString(int length) {
char[] text = new char[length];
for (int i = 0; i < length; i++) {
text[i] = ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length()));
}
return new String(text);
}
/**
* 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 headers the {@link JSONObject} where the headers should be set.
* @param ttlInSecs sets the {@link RestUtils.Headers#TTL} header.
* @param isPrivate sets the {@link RestUtils.Headers#PRIVATE} header. Allowed values: true, false.
* @param serviceId sets the {@link RestUtils.Headers#SERVICE_ID} header.
* @param contentType sets the {@link RestUtils.Headers#AMBRY_CONTENT_TYPE} header.
* @param ownerId sets the {@link RestUtils.Headers#OWNER_ID} header. Optional - if not required, send null.
* @throws JSONException
*/
private void setAmbryHeadersForPut(JSONObject headers, String ttlInSecs, String isPrivate, String serviceId,
String contentType, String ownerId) throws JSONException {
headers.putOpt(RestUtils.Headers.TTL, ttlInSecs);
headers.putOpt(RestUtils.Headers.PRIVATE, isPrivate);
headers.putOpt(RestUtils.Headers.SERVICE_ID, serviceId);
headers.putOpt(RestUtils.Headers.AMBRY_CONTENT_TYPE, contentType);
headers.putOpt(RestUtils.Headers.OWNER_ID, ownerId);
}
/**
* Verifies that a request with headers defined by {@code headers} builds {@link BlobProperties} successfully and
* matches the values of the properties with those in {@code headers}.
* @param headers the headers that need to go with the request that is used to construct {@link BlobProperties}.
* @throws Exception
*/
private void verifyBlobPropertiesConstructionSuccess(JSONObject headers) throws Exception {
RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers);
BlobProperties blobProperties = RestUtils.buildBlobProperties(restRequest.getArgs());
long expectedTTL = Utils.Infinite_Time;
if (headers.has(RestUtils.Headers.TTL) && !JSONObject.NULL.equals(headers.get(RestUtils.Headers.TTL))) {
expectedTTL = headers.getLong(RestUtils.Headers.TTL);
}
assertEquals("Blob TTL does not match", expectedTTL, blobProperties.getTimeToLiveInSeconds());
boolean expectedIsPrivate = false;
if (headers.has(RestUtils.Headers.PRIVATE) && !JSONObject.NULL.equals(headers.get(RestUtils.Headers.PRIVATE))) {
expectedIsPrivate = headers.getBoolean(RestUtils.Headers.PRIVATE);
}
assertEquals("Blob isPrivate does not match", expectedIsPrivate, blobProperties.isPrivate());
assertEquals("Blob service ID does not match", headers.getString(RestUtils.Headers.SERVICE_ID),
blobProperties.getServiceId());
assertEquals("Blob content type does not match", headers.getString(RestUtils.Headers.AMBRY_CONTENT_TYPE),
blobProperties.getContentType());
if (headers.has(RestUtils.Headers.OWNER_ID) && !JSONObject.NULL.equals(headers.get(RestUtils.Headers.OWNER_ID))) {
assertEquals("Blob owner ID does not match", headers.getString(RestUtils.Headers.OWNER_ID),
blobProperties.getOwnerId());
}
}
/**
* Verifies that a request with headers defined by {@code headers} builds UserMetadata successfully and
* matches the values with those in {@code headers}.
* @param headers the headers that need to go with the request that is used to construct the User Metadata
* @throws Exception
*/
private void verifyUserMetadataConstructionSuccess(JSONObject headers, Map<String, String> inputUserMetadata)
throws Exception {
RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers);
byte[] userMetadata = RestUtils.buildUsermetadata(restRequest.getArgs());
Map<String, String> userMetadataMap = RestUtils.buildUserMetadata(userMetadata);
assertEquals("Total number of entries doesnt match ", inputUserMetadata.size(), userMetadataMap.size());
for (String key : userMetadataMap.keySet()) {
boolean keyFromInputMap = inputUserMetadata.containsKey(key);
assertTrue("Key " + key + " not found in input user metadata", keyFromInputMap);
assertTrue("Values didn't match for key " + key + ", value from input map value " + inputUserMetadata.get(key)
+ ", and output map value " + userMetadataMap.get(key),
inputUserMetadata.get(key).equals(userMetadataMap.get(key)));
}
}
// getBlobPropertiesVariedInputTest() helpers.
/**
* Verifies that {@link RestUtils#buildBlobProperties(Map<String,Object>)} fails if given a request with bad
* arguments.
* @param headers the headers that were provided to the request.
* @param expectedCode the expected {@link RestServiceErrorCode} because of the failure.
* @throws JSONException
* @throws UnsupportedEncodingException
* @throws URISyntaxException
*/
private void verifyBlobPropertiesConstructionFailure(JSONObject headers, RestServiceErrorCode expectedCode)
throws JSONException, UnsupportedEncodingException, URISyntaxException {
try {
RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers);
RestUtils.buildBlobProperties(restRequest.getArgs());
fail("An exception was expected but none were thrown");
} catch (RestServiceException e) {
assertEquals("Unexpected RestServiceErrorCode", expectedCode, e.getErrorCode());
}
}
/**
* Adds extra values for the header {@code extraValueHeader} and tests that the right exception is thrown.
* @param headers the headers that need to go with the request that is used to construct {@link BlobProperties}.
* @param extraValueHeader the header for which extra values will be added.
* @throws JSONException
* @throws UnsupportedEncodingException
* @throws URISyntaxException
*/
private void tooManyValuesTest(JSONObject headers, String extraValueHeader)
throws JSONException, UnsupportedEncodingException, URISyntaxException {
String uri = "?" + extraValueHeader + "=extraVal1&" + extraValueHeader + "=extraVal2";
try {
RestRequest restRequest = createRestRequest(RestMethod.POST, uri, headers);
RestUtils.buildBlobProperties(restRequest.getArgs());
fail("An exception was expected but none were thrown");
} catch (RestServiceException e) {
assertEquals("Unexpected RestServiceErrorCode", RestServiceErrorCode.InvalidArgs, e.getErrorCode());
}
}
/**
* Test that {@link RestUtils#buildGetBlobOptions(Map, RestUtils.SubResource, GetOption)} works correctly for a given
* range with and without a specified sub-resource.
* @param rangeHeader the Range header value to add to the {@code args} map.
* @param expectedRange the {@link ByteRange} expected to be parsed if the call should succeed, or {@code null} if no
* range is expected.
* @param shouldSucceedWithoutSubResource {@code true} if the call should succeed with no specified sub-resource.
* @param shouldSucceedWithSubResource {@code true} if the call should succeed with a specified sub-resource.
* @throws RestServiceException
*/
private void doBuildGetBlobOptionsTest(String rangeHeader, ByteRange expectedRange,
boolean shouldSucceedWithoutSubResource, boolean shouldSucceedWithSubResource) throws RestServiceException {
Map<String, Object> args = new HashMap<>();
if (rangeHeader != null) {
args.put(RestUtils.Headers.RANGE, rangeHeader);
}
doBuildGetBlobOptionsTestForSubResource(args, null, expectedRange, GetBlobOptions.OperationType.All,
shouldSucceedWithoutSubResource);
for (RestUtils.SubResource subResource : RestUtils.SubResource.values()) {
doBuildGetBlobOptionsTestForSubResource(args, subResource, expectedRange, GetBlobOptions.OperationType.BlobInfo,
shouldSucceedWithSubResource);
}
}
/**
* Test that {@link RestUtils#buildGetBlobOptions(Map, RestUtils.SubResource, GetOption)} works correctly with given args and a
* specified sub-resource.
* @param args the map of args for the method call.
* @param subResource the sub-resource for the call.
* @param expectedRange the {@link ByteRange} expected to be parsed if the call should succeed, or {@code null} if no
* range is expected.
* @param expectedOpType the {@link GetBlobOptions.OperationType} expected to be set in the {@link GetBlobOptions}
* object.
* @param shouldSucceed {@code true} if the call should succeed.
* @throws RestServiceException
*/
private void doBuildGetBlobOptionsTestForSubResource(Map<String, Object> args, RestUtils.SubResource subResource,
ByteRange expectedRange, GetBlobOptions.OperationType expectedOpType, boolean shouldSucceed)
throws RestServiceException {
if (shouldSucceed) {
GetBlobOptions options = RestUtils.buildGetBlobOptions(args, subResource, GetOption.None);
assertEquals("Unexpected range for args=" + args + " and subResource=" + subResource, expectedRange,
options.getRange());
assertEquals("Unexpected operation type for args=" + args + " and subResource=" + subResource, expectedOpType,
options.getOperationType());
assertEquals("Unexpected get options type for args=" + args + " and subResource=" + subResource, GetOption.None,
options.getGetOption());
} else {
try {
RestUtils.buildGetBlobOptions(args, subResource, GetOption.None);
fail("buildGetBlobOptions should not have succeeded with args=" + args + "and subResource=" + subResource);
} catch (RestServiceException expected) {
assertEquals("Unexpected error code.", RestServiceErrorCode.InvalidArgs, expected.getErrorCode());
}
}
}
/**
* Test {@link RestUtils#buildContentRangeAndLength(ByteRange, long)} for a specific {@link ByteRange} and total blob
* size.
* @param range the {@link ByteRange} to test for.
* @param blobSize the total blob size in bytes to test for.
* @param expectedContentRange the expected Content-Range header string.
* @param expectedContentLength the expected Content-Length in bytes.
* @param shouldSucceed {@code true} if the call should succeed, {@code false} if an error is expected.
* @throws RestServiceException
*/
private void doBuildContentRangeAndLengthTest(ByteRange range, long blobSize, String expectedContentRange,
long expectedContentLength, boolean shouldSucceed) throws RestServiceException {
if (shouldSucceed) {
Pair<String, Long> rangeAndLength = RestUtils.buildContentRangeAndLength(range, blobSize);
assertEquals(expectedContentRange, rangeAndLength.getFirst());
assertEquals(expectedContentLength, (long) rangeAndLength.getSecond());
} else {
try {
RestUtils.buildContentRangeAndLength(range, blobSize);
fail("Should have encountered exception when building Content-Range");
} catch (RestServiceException e) {
assertEquals("Unexpected error code.", RestServiceErrorCode.RangeNotSatisfiable, e.getErrorCode());
}
}
}
/**
* Sets entries from the passed in HashMap to the @{link JSONObject} headers
* @param headers {@link JSONObject} to which the new headers are to be added
* @param userMetadata {@link Map} which has the new entries that has to be added
* @throws org.json.JSONException
*/
public static void setUserMetadataHeaders(JSONObject headers, Map<String, String> userMetadata) throws JSONException {
for (String key : userMetadata.keySet()) {
headers.put(key, userMetadata.get(key));
}
}
}