/** * 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.codahale.metrics.MetricRegistry; import com.github.ambry.commons.ByteBufferReadableStreamChannel; import com.github.ambry.config.FrontendConfig; import com.github.ambry.config.VerifiableProperties; import com.github.ambry.messageformat.BlobInfo; import com.github.ambry.messageformat.BlobProperties; import com.github.ambry.rest.IdConverter; import com.github.ambry.rest.IdConverterFactory; import com.github.ambry.rest.MockRestRequest; import com.github.ambry.rest.MockRestResponseChannel; import com.github.ambry.rest.ResponseStatus; import com.github.ambry.rest.RestMethod; import com.github.ambry.rest.RestRequest; import com.github.ambry.rest.RestRequestMetricsTracker; import com.github.ambry.rest.RestResponseChannel; import com.github.ambry.rest.RestResponseHandler; import com.github.ambry.rest.RestServiceErrorCode; import com.github.ambry.rest.RestServiceException; import com.github.ambry.rest.RestTestUtils; import com.github.ambry.rest.RestUtils; import com.github.ambry.rest.RestUtilsTest; import com.github.ambry.rest.SecurityService; import com.github.ambry.rest.SecurityServiceFactory; import com.github.ambry.router.AsyncWritableChannel; import com.github.ambry.router.ByteRange; import com.github.ambry.router.Callback; import com.github.ambry.router.FutureResult; import com.github.ambry.router.GetBlobOptions; import com.github.ambry.router.GetBlobResult; import com.github.ambry.router.InMemoryRouter; import com.github.ambry.router.ReadableStreamChannel; import com.github.ambry.router.Router; import com.github.ambry.router.RouterErrorCode; import com.github.ambry.router.RouterException; 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 java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.TimeZone; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLSession; import org.json.JSONException; import org.json.JSONObject; import org.junit.After; import org.junit.Test; import static org.junit.Assert.*; /** * Unit tests for {@link AmbryBlobStorageService}. */ public class AmbryBlobStorageServiceTest { private final MetricRegistry metricRegistry = new MetricRegistry(); private final FrontendMetrics frontendMetrics = new FrontendMetrics(metricRegistry); private final FrontendConfig frontendConfig; private final IdConverterFactory idConverterFactory; private final SecurityServiceFactory securityServiceFactory; private final FrontendTestResponseHandler responseHandler; private final InMemoryRouter router; private AmbryBlobStorageService ambryBlobStorageService; /** * Sets up the {@link AmbryBlobStorageService} instance before a test. * @throws InstantiationException */ public AmbryBlobStorageServiceTest() throws InstantiationException { VerifiableProperties verifiableProperties = new VerifiableProperties(new Properties()); RestRequestMetricsTracker.setDefaults(metricRegistry); frontendConfig = new FrontendConfig(verifiableProperties); idConverterFactory = new AmbryIdConverterFactory(verifiableProperties, metricRegistry); securityServiceFactory = new AmbrySecurityServiceFactory(verifiableProperties, metricRegistry); router = new InMemoryRouter(verifiableProperties); responseHandler = new FrontendTestResponseHandler(); ambryBlobStorageService = getAmbryBlobStorageService(); responseHandler.start(); ambryBlobStorageService.start(); } /** * Shuts down the {@link AmbryBlobStorageService} instance after all tests. * @throws IOException */ @After public void shutdownAmbryBlobStorageService() throws IOException { ambryBlobStorageService.shutdown(); responseHandler.shutdown(); router.close(); } /** * Tests basic startup and shutdown functionality (no exceptions). * @throws InstantiationException * @throws IOException */ @Test public void startShutDownTest() throws InstantiationException, IOException { ambryBlobStorageService.start(); ambryBlobStorageService.shutdown(); } /** * Tests for {@link AmbryBlobStorageService#shutdown()} when {@link AmbryBlobStorageService#start()} has not been * called previously. * <p/> * This test is for cases where {@link AmbryBlobStorageService#start()} has failed and * {@link AmbryBlobStorageService#shutdown()} needs to be run. * @throws IOException */ @Test public void shutdownWithoutStartTest() throws IOException { AmbryBlobStorageService ambryBlobStorageService = getAmbryBlobStorageService(); ambryBlobStorageService.shutdown(); } /** * This tests for exceptions thrown when an {@link AmbryBlobStorageService} instance is used without calling * {@link AmbryBlobStorageService#start()} first. * @throws Exception */ @Test public void useServiceWithoutStartTest() throws Exception { ambryBlobStorageService = getAmbryBlobStorageService(); // not fine to use without start. try { doOperation(createRestRequest(RestMethod.GET, "/", null, null), new MockRestResponseChannel()); fail("Should not have been able to use AmbryBlobStorageService without start"); } catch (RestServiceException e) { assertEquals("Unexpected RestServiceErrorCode", RestServiceErrorCode.ServiceUnavailable, e.getErrorCode()); } } /** * Checks for reactions of all methods in {@link AmbryBlobStorageService} to null arguments. * @throws Exception */ @Test public void nullInputsForFunctionsTest() throws Exception { doNullInputsForFunctionsTest("handleGet"); doNullInputsForFunctionsTest("handlePost"); doNullInputsForFunctionsTest("handleDelete"); doNullInputsForFunctionsTest("handleHead"); } /** * Checks reactions of all methods in {@link AmbryBlobStorageService} to a {@link Router} that throws * {@link RuntimeException}. * @throws Exception */ @Test public void runtimeExceptionRouterTest() throws Exception { // set InMemoryRouter up to throw RuntimeException Properties properties = new Properties(); properties.setProperty(InMemoryRouter.OPERATION_THROW_EARLY_RUNTIME_EXCEPTION, "true"); router.setVerifiableProperties(new VerifiableProperties(properties)); doRuntimeExceptionRouterTest(RestMethod.GET); doRuntimeExceptionRouterTest(RestMethod.POST); doRuntimeExceptionRouterTest(RestMethod.DELETE); doRuntimeExceptionRouterTest(RestMethod.HEAD); } /** * Checks reactions of PUT methods in {@link AmbryBlobStorageService} * @throws Exception */ @Test public void putFailureTest() throws Exception { RestRequest restRequest = createRestRequest(RestMethod.PUT, "/", null, null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); try { doOperation(restRequest, restResponseChannel); fail("PUT should have failed because Ambry does not support it"); } catch (RestServiceException e) { assertEquals("PUT is an unsupported method", RestServiceErrorCode.UnsupportedHttpMethod, e.getErrorCode()); } } /** * Checks reactions of all methods in {@link AmbryBlobStorageService} to bad {@link RestResponseHandler} and * {@link RestRequest} implementations. * @throws Exception */ @Test public void badResponseHandlerAndRestRequestTest() throws Exception { RestRequest restRequest = new BadRestRequest(); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); // What happens inside AmbryBlobStorageService during this test? // 1. Since the RestRequest throws errors, AmbryBlobStorageService will attempt to submit response with exception // to FrontendTestResponseHandler. // 2. The submission will fail because FrontendTestResponseHandler has been shutdown. // 3. AmbryBlobStorageService will directly complete the request over the RestResponseChannel with the *original* // exception. // 4. It will then try to release resources but closing the RestRequest will also throw an exception. This exception // is swallowed. // What the test is looking for -> No exceptions thrown when the handle is run and the original exception arrives // safely. responseHandler.shutdown(); ambryBlobStorageService.handleGet(restRequest, restResponseChannel); // IllegalStateException is thrown in BadRestRequest. assertEquals("Unexpected exception", IllegalStateException.class, restResponseChannel.getException().getClass()); responseHandler.reset(); restResponseChannel = new MockRestResponseChannel(); ambryBlobStorageService.handlePost(restRequest, restResponseChannel); // IllegalStateException or NullPointerException is thrown because of BadRestRequest. Exception e = restResponseChannel.getException(); assertTrue("Unexpected exception", e instanceof IllegalStateException || e instanceof NullPointerException); responseHandler.reset(); restResponseChannel = new MockRestResponseChannel(); ambryBlobStorageService.handleDelete(restRequest, restResponseChannel); // IllegalStateException or NullPointerException is thrown because of BadRestRequest. e = restResponseChannel.getException(); assertTrue("Unexpected exception", e instanceof IllegalStateException || e instanceof NullPointerException); responseHandler.reset(); restResponseChannel = new MockRestResponseChannel(); ambryBlobStorageService.handleHead(restRequest, restResponseChannel); // IllegalStateException or NullPointerException is thrown because of BadRestRequest. e = restResponseChannel.getException(); assertTrue("Unexpected exception", e instanceof IllegalStateException || e instanceof NullPointerException); } /** * Tests * {@link AmbryBlobStorageService#submitResponse(RestRequest, RestResponseChannel, ReadableStreamChannel, Exception)}. * @throws JSONException * @throws UnsupportedEncodingException * @throws URISyntaxException */ @Test public void submitResponseTest() throws JSONException, UnsupportedEncodingException, URISyntaxException { String exceptionMsg = UtilsTest.getRandomString(10); responseHandler.shutdown(); // handleResponse of FrontendTestResponseHandler throws exception because it has been shutdown. try { // there is an exception already. RestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null); assertTrue("RestRequest channel is not open", restRequest.isOpen()); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); ambryBlobStorageService.submitResponse(restRequest, restResponseChannel, null, new RuntimeException(exceptionMsg)); assertEquals("Unexpected exception message", exceptionMsg, restResponseChannel.getException().getMessage()); // there is no exception and exception thrown when the response is submitted. restRequest = createRestRequest(RestMethod.GET, "/", null, null); assertTrue("RestRequest channel is not open", restRequest.isOpen()); restResponseChannel = new MockRestResponseChannel(); ReadableStreamChannel response = new ByteBufferReadableStreamChannel(ByteBuffer.allocate(0)); assertTrue("Response channel is not open", response.isOpen()); ambryBlobStorageService.submitResponse(restRequest, restResponseChannel, response, null); assertNotNull("There is no cause of failure", restResponseChannel.getException()); // resources should have been cleaned up. assertFalse("Response channel is not cleaned up", response.isOpen()); } finally { responseHandler.start(); } } /** * Tests releasing of resources if response submission fails. * @throws JSONException * @throws UnsupportedEncodingException * @throws URISyntaxException */ @Test public void releaseResourcesTest() throws JSONException, UnsupportedEncodingException, URISyntaxException { responseHandler.shutdown(); // handleResponse of FrontendTestResponseHandler throws exception because it has been shutdown. try { RestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.allocate(0)); assertTrue("RestRequest channel not open", restRequest.isOpen()); assertTrue("ReadableStreamChannel not open", channel.isOpen()); ambryBlobStorageService.submitResponse(restRequest, restResponseChannel, channel, null); assertFalse("ReadableStreamChannel is still open", channel.isOpen()); // null ReadableStreamChannel restRequest = createRestRequest(RestMethod.GET, "/", null, null); restResponseChannel = new MockRestResponseChannel(); assertTrue("RestRequest channel not open", restRequest.isOpen()); ambryBlobStorageService.submitResponse(restRequest, restResponseChannel, null, null); // bad RestRequest (close() throws IOException) channel = new ByteBufferReadableStreamChannel(ByteBuffer.allocate(0)); restResponseChannel = new MockRestResponseChannel(); assertTrue("ReadableStreamChannel not open", channel.isOpen()); ambryBlobStorageService.submitResponse(new BadRestRequest(), restResponseChannel, channel, null); // bad ReadableStreamChannel (close() throws IOException) restRequest = createRestRequest(RestMethod.GET, "/", null, null); restResponseChannel = new MockRestResponseChannel(); assertTrue("RestRequest channel not open", restRequest.isOpen()); ambryBlobStorageService.submitResponse(restRequest, restResponseChannel, new BadRSC(), null); } finally { responseHandler.start(); } } /** * Tests blob POST, GET, HEAD and DELETE operations. * @throws Exception */ @Test public void postGetHeadDeleteTest() throws Exception { final int CONTENT_LENGTH = 1024; ByteBuffer content = ByteBuffer.wrap(TestUtils.getRandomBytes(CONTENT_LENGTH)); String serviceId = "postGetHeadDeleteServiceID"; String contentType = "application/octet-stream"; String ownerId = "postGetHeadDeleteOwnerID"; JSONObject headers = new JSONObject(); setAmbryHeadersForPut(headers, 7200, false, serviceId, contentType, ownerId); 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"); RestUtilsTest.setUserMetadataHeaders(headers, userMetadata); String blobId = postBlobAndVerify(headers, content); headers.put(RestUtils.Headers.BLOB_SIZE, (long) CONTENT_LENGTH); getBlobAndVerify(blobId, null, headers, content); getHeadAndVerify(blobId, null, headers); ByteRange range = ByteRange.fromStartOffset(ThreadLocalRandom.current().nextLong(CONTENT_LENGTH)); getBlobAndVerify(blobId, range, headers, content); getHeadAndVerify(blobId, range, headers); range = ByteRange.fromLastNBytes(ThreadLocalRandom.current().nextLong(CONTENT_LENGTH + 1)); getBlobAndVerify(blobId, range, headers, content); getHeadAndVerify(blobId, range, headers); long random1 = ThreadLocalRandom.current().nextLong(CONTENT_LENGTH); long random2 = ThreadLocalRandom.current().nextLong(CONTENT_LENGTH); range = ByteRange.fromOffsetRange(Math.min(random1, random2), Math.max(random1, random2)); getBlobAndVerify(blobId, range, headers, content); getHeadAndVerify(blobId, range, headers); getNotModifiedBlobAndVerify(blobId); getUserMetadataAndVerify(blobId, headers); getBlobInfoAndVerify(blobId, headers); deleteBlobAndVerify(blobId); // check GET, HEAD and DELETE after delete. verifyOperationsAfterDelete(blobId); } /** * Tests how metadata that has not been POSTed in the form of headers is returned. * @throws Exception */ @Test public void oldStyleUserMetadataTest() throws Exception { ByteBuffer content = ByteBuffer.allocate(0); BlobProperties blobProperties = new BlobProperties(0, "userMetadataTestOldStyleServiceID"); byte[] usermetadata = TestUtils.getRandomBytes(25); String blobId = router.putBlob(blobProperties, usermetadata, new ByteBufferReadableStreamChannel(content)).get(); RestUtils.SubResource[] subResources = {RestUtils.SubResource.UserMetadata, RestUtils.SubResource.BlobInfo}; for (RestUtils.SubResource subResource : subResources) { RestRequest restRequest = createRestRequest(RestMethod.GET, blobId + "/" + subResource, null, null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status for " + subResource, ResponseStatus.Ok, restResponseChannel.getStatus()); assertEquals("Unexpected Content-Type for " + subResource, "application/octet-stream", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_TYPE)); assertEquals("Unexpected Content-Length for " + subResource, usermetadata.length, Integer.parseInt(restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH))); assertArrayEquals("Unexpected user metadata for " + subResource, usermetadata, restResponseChannel.getResponseBody()); } } /** * Tests for cases where the {@link IdConverter} misbehaves and throws {@link RuntimeException}. * @throws InstantiationException * @throws JSONException */ @Test public void misbehavingIdConverterTest() throws InstantiationException, JSONException { FrontendTestIdConverterFactory converterFactory = new FrontendTestIdConverterFactory(); String exceptionMsg = UtilsTest.getRandomString(10); converterFactory.exceptionToThrow = new IllegalStateException(exceptionMsg); doIdConverterExceptionTest(converterFactory, exceptionMsg); } /** * Tests for cases where the {@link IdConverter} returns valid exceptions. * @throws InstantiationException * @throws JSONException */ @Test public void idConverterExceptionPipelineTest() throws InstantiationException, JSONException { FrontendTestIdConverterFactory converterFactory = new FrontendTestIdConverterFactory(); String exceptionMsg = UtilsTest.getRandomString(10); converterFactory.exceptionToReturn = new IllegalStateException(exceptionMsg); doIdConverterExceptionTest(converterFactory, exceptionMsg); } /** * Tests for cases where the {@link SecurityService} misbehaves and throws {@link RuntimeException}. * @throws InstantiationException * @throws JSONException */ @Test public void misbehavingSecurityServiceTest() throws InstantiationException, JSONException { FrontendTestSecurityServiceFactory securityFactory = new FrontendTestSecurityServiceFactory(); String exceptionMsg = UtilsTest.getRandomString(10); securityFactory.exceptionToThrow = new IllegalStateException(exceptionMsg); doSecurityServiceExceptionTest(securityFactory, exceptionMsg); } /** * Tests for cases where the {@link SecurityService} returns valid exceptions. * @throws InstantiationException * @throws JSONException */ @Test public void securityServiceExceptionPipelineTest() throws InstantiationException, JSONException { FrontendTestSecurityServiceFactory securityFactory = new FrontendTestSecurityServiceFactory(); String exceptionMsg = UtilsTest.getRandomString(10); securityFactory.exceptionToReturn = new IllegalStateException(exceptionMsg); doSecurityServiceExceptionTest(securityFactory, exceptionMsg); } /** * Tests for cases where the {@link Router} misbehaves and throws {@link RuntimeException}. * @throws Exception */ @Test public void misbehavingRouterTest() throws Exception { FrontendTestRouter testRouter = new FrontendTestRouter(); String exceptionMsg = UtilsTest.getRandomString(10); testRouter.exceptionToThrow = new IllegalStateException(exceptionMsg); doRouterExceptionPipelineTest(testRouter, exceptionMsg); } /** * Tests for cases where the {@link Router} returns valid {@link RouterException}. * @throws InstantiationException * @throws JSONException */ @Test public void routerExceptionPipelineTest() throws Exception { FrontendTestRouter testRouter = new FrontendTestRouter(); String exceptionMsg = UtilsTest.getRandomString(10); testRouter.exceptionToReturn = new RouterException(exceptionMsg, RouterErrorCode.UnexpectedInternalError); doRouterExceptionPipelineTest(testRouter, exceptionMsg); } /** * Test that GET operations fail with the expected error code when a bad range header is provided. * @throws Exception */ @Test public void badRangeHeaderTest() throws Exception { JSONObject headers = new JSONObject(); headers.put(RestUtils.Headers.RANGE, "adsfksakdfsdfkdaklf"); try { doOperation(createRestRequest(RestMethod.GET, "/", headers, null), new MockRestResponseChannel()); fail("GET operation should have failed because of an invalid range header"); } catch (RestServiceException e) { assertEquals("Unexpected error code", RestServiceErrorCode.InvalidArgs, e.getErrorCode()); } } /** * Test that the correct service ID is sent to the router on deletes. * @throws Exception */ @Test public void deleteServiceIdTest() throws Exception { FrontendTestRouter testRouter = new FrontendTestRouter(); ambryBlobStorageService = new AmbryBlobStorageService(frontendConfig, frontendMetrics, responseHandler, testRouter, idConverterFactory, securityServiceFactory); ambryBlobStorageService.start(); JSONObject headers = new JSONObject(); String serviceId = "service-id"; headers.put(RestUtils.Headers.SERVICE_ID, serviceId); doOperation(createRestRequest(RestMethod.DELETE, "/", headers, null), new MockRestResponseChannel()); assertEquals(serviceId, testRouter.deleteServiceId); doOperation(createRestRequest(RestMethod.DELETE, "/", null, null), new MockRestResponseChannel()); assertNull("Service ID should not have been set for this delete", testRouter.deleteServiceId); } // 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 JSONObject}. * @param contents the content that accompanies the request. * @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, List<ByteBuffer> contents) 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, contents); } /** * 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. 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. * @throws JSONException */ private void setAmbryHeadersForPut(JSONObject headers, long ttlInSecs, boolean isPrivate, String serviceId, String contentType, String ownerId) throws JSONException { if (headers != null && ttlInSecs >= -1 && serviceId != null && contentType != null) { headers.put(RestUtils.Headers.TTL, ttlInSecs); headers.put(RestUtils.Headers.PRIVATE, isPrivate); headers.put(RestUtils.Headers.SERVICE_ID, serviceId); headers.put(RestUtils.Headers.AMBRY_CONTENT_TYPE, contentType); if (ownerId != null) { headers.put(RestUtils.Headers.OWNER_ID, ownerId); } } else { throw new IllegalArgumentException("Some required arguments are null. Cannot set ambry headers"); } } /** * Does an operation in {@link AmbryBlobStorageService} as dictated by the {@link RestMethod} in {@code restRequest} * and returns the result, if any. If an exception occurs during the operation, throws the exception. * @param restRequest the {@link RestRequest} that needs to be submitted to the {@link AmbryBlobStorageService}. * @param restResponseChannel the {@link RestResponseChannel} to use to return the response. * @throws Exception */ private void doOperation(RestRequest restRequest, RestResponseChannel restResponseChannel) throws Exception { responseHandler.reset(); switch (restRequest.getRestMethod()) { case POST: ambryBlobStorageService.handlePost(restRequest, restResponseChannel); break; case PUT: ambryBlobStorageService.handlePut(restRequest, restResponseChannel); break; case GET: ambryBlobStorageService.handleGet(restRequest, restResponseChannel); break; case DELETE: ambryBlobStorageService.handleDelete(restRequest, restResponseChannel); break; case HEAD: ambryBlobStorageService.handleHead(restRequest, restResponseChannel); break; default: fail("RestMethod not supported: " + restRequest.getRestMethod()); } if (responseHandler.awaitResponseSubmission(1, TimeUnit.SECONDS)) { if (responseHandler.getException() != null) { throw responseHandler.getException(); } } else { throw new IllegalStateException("doOperation() timed out"); } } // Constructor helpers /** * Sets up and gets an instance of {@link AmbryBlobStorageService}. * @return an instance of {@link AmbryBlobStorageService}. */ private AmbryBlobStorageService getAmbryBlobStorageService() { return new AmbryBlobStorageService(frontendConfig, frontendMetrics, responseHandler, router, idConverterFactory, securityServiceFactory); } // nullInputsForFunctionsTest() helpers /** * Checks for reaction to null input in {@code methodName} in {@link AmbryBlobStorageService}. * @param methodName the name of the method to invoke. * @throws Exception */ private void doNullInputsForFunctionsTest(String methodName) throws Exception { Method method = AmbryBlobStorageService.class.getDeclaredMethod(methodName, RestRequest.class, RestResponseChannel.class); RestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null); RestResponseChannel restResponseChannel = new MockRestResponseChannel(); responseHandler.reset(); try { method.invoke(ambryBlobStorageService, null, restResponseChannel); fail("Method [" + methodName + "] should have failed because RestRequest is null"); } catch (InvocationTargetException e) { assertEquals("Unexpected exception class", IllegalArgumentException.class, e.getTargetException().getClass()); } responseHandler.reset(); try { method.invoke(ambryBlobStorageService, restRequest, null); fail("Method [" + methodName + "] should have failed because RestResponseChannel is null"); } catch (InvocationTargetException e) { assertEquals("Unexpected exception class", IllegalArgumentException.class, e.getTargetException().getClass()); } } // runtimeExceptionRouterTest() helpers /** * Tests reactions of various methods of {@link AmbryBlobStorageService} to a {@link Router} that throws * {@link RuntimeException}. * @param restMethod used to determine the method to invoke in {@link AmbryBlobStorageService}. * @throws Exception */ private void doRuntimeExceptionRouterTest(RestMethod restMethod) throws Exception { RestRequest restRequest = createRestRequest(restMethod, "/", null, null); RestResponseChannel restResponseChannel = new MockRestResponseChannel(); try { switch (restMethod) { case GET: case DELETE: case HEAD: doOperation(restRequest, restResponseChannel); fail(restMethod + " should have detected a RestServiceException because of a bad router"); break; case POST: JSONObject headers = new JSONObject(); setAmbryHeadersForPut(headers, Utils.Infinite_Time, false, "test-serviceID", "text/plain", "test-ownerId"); restRequest = createRestRequest(restMethod, "/", headers, null); doOperation(restRequest, restResponseChannel); fail("POST should have detected a RestServiceException because of a bad router"); break; default: throw new IllegalArgumentException("Unrecognized RestMethod: " + restMethod); } } catch (RuntimeException e) { assertEquals("Unexpected error message", InMemoryRouter.OPERATION_THROW_EARLY_RUNTIME_EXCEPTION, e.getMessage()); } } // postGetHeadDeleteTest() helpers /** * Posts a blob with the given {@code headers} and {@code content}. * @param headers the headers of the new blob that get converted to blob properties. * @param content the content of the blob. * @return the blob ID of the blob. * @throws Exception */ public String postBlobAndVerify(JSONObject headers, ByteBuffer content) throws Exception { List<ByteBuffer> contents = new LinkedList<ByteBuffer>(); contents.add(content); contents.add(null); RestRequest restRequest = createRestRequest(RestMethod.POST, "/", headers, contents); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", ResponseStatus.Created, restResponseChannel.getStatus()); assertTrue("No Date header", restResponseChannel.getHeader(RestUtils.Headers.DATE) != null); assertTrue("No " + RestUtils.Headers.CREATION_TIME, restResponseChannel.getHeader(RestUtils.Headers.CREATION_TIME) != null); assertEquals("Content-Length is not 0", "0", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH)); assertNull("Content-Range header should not be set", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); String blobId = restResponseChannel.getHeader(RestUtils.Headers.LOCATION); if (blobId == null) { fail("postBlobAndVerify did not return a blob ID"); } 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 optional {@link ByteRange} for the request. * @param expectedHeaders the expected headers in the response. * @param expectedContent the expected content of the blob. * @throws Exception */ private void getBlobAndVerify(String blobId, ByteRange range, JSONObject expectedHeaders, ByteBuffer expectedContent) throws Exception { RestRequest restRequest = createRestRequest(RestMethod.GET, blobId, createRequestHeaders(range), null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", range == null ? ResponseStatus.Ok : ResponseStatus.PartialContent, restResponseChannel.getStatus()); checkCommonGetHeadHeaders(restResponseChannel); assertEquals(RestUtils.Headers.BLOB_SIZE + " does not match", expectedHeaders.getString(RestUtils.Headers.BLOB_SIZE), restResponseChannel.getHeader(RestUtils.Headers.BLOB_SIZE)); assertEquals("Content-Type does not match", expectedHeaders.getString(RestUtils.Headers.AMBRY_CONTENT_TYPE), restResponseChannel.getHeader(RestUtils.Headers.CONTENT_TYPE)); assertEquals("Accept-Ranges not set correctly", "bytes", restResponseChannel.getHeader(RestUtils.Headers.ACCEPT_RANGES)); byte[] expectedContentArray = expectedContent.array(); if (range != null) { long blobSize = expectedHeaders.getLong(RestUtils.Headers.BLOB_SIZE); assertEquals("Content-Range does not match expected", RestUtils.buildContentRangeAndLength(range, blobSize).getFirst(), restResponseChannel.getHeader(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", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); } assertArrayEquals("GET content does not match original content", expectedContentArray, restResponseChannel.getResponseBody()); } /** * 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. * @throws Exception */ private void getNotModifiedBlobAndVerify(String blobId) throws Exception { JSONObject headers = new JSONObject(); SimpleDateFormat dateFormat = new SimpleDateFormat(RestUtils.HTTP_DATE_FORMAT, Locale.ENGLISH); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); Date date = new Date(System.currentTimeMillis()); String dateStr = dateFormat.format(date); headers.put(RestUtils.Headers.IF_MODIFIED_SINCE, dateStr); RestRequest restRequest = createRestRequest(RestMethod.GET, blobId, headers, null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", ResponseStatus.NotModified, restResponseChannel.getStatus()); assertNotNull("Date header expected", restResponseChannel.getHeader(RestUtils.Headers.DATE)); assertNotNull("Last-Modified header expected", restResponseChannel.getHeader(RestUtils.Headers.LAST_MODIFIED)); assertNull(RestUtils.Headers.BLOB_SIZE + " should have been null ", restResponseChannel.getHeader(RestUtils.Headers.BLOB_SIZE)); assertNull("Content-Type should have been null", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_TYPE)); assertNull("Content-Length should have been null", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH)); assertEquals("No content expected as blob is not modified", 0, restResponseChannel.getResponseBody().length); assertNull("Accept-Ranges should not be set", restResponseChannel.getHeader(RestUtils.Headers.ACCEPT_RANGES)); assertNull("Content-Range header should not be set", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); } /** * 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. * @throws Exception */ private void getUserMetadataAndVerify(String blobId, JSONObject expectedHeaders) throws Exception { RestRequest restRequest = createRestRequest(RestMethod.GET, blobId + "/" + RestUtils.SubResource.UserMetadata, null, null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", ResponseStatus.Ok, restResponseChannel.getStatus()); checkCommonGetHeadHeaders(restResponseChannel); assertEquals("Content-Length is not 0", "0", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH)); assertNull("Accept-Ranges should not be set", restResponseChannel.getHeader(RestUtils.Headers.ACCEPT_RANGES)); assertNull("Content-Range header should not be set", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); verifyUserMetadataHeaders(expectedHeaders, restResponseChannel); } /** * 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. * @throws Exception */ private void getBlobInfoAndVerify(String blobId, JSONObject expectedHeaders) throws Exception { RestRequest restRequest = createRestRequest(RestMethod.GET, blobId + "/" + RestUtils.SubResource.BlobInfo, null, null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", ResponseStatus.Ok, restResponseChannel.getStatus()); checkCommonGetHeadHeaders(restResponseChannel); assertEquals("Content-Length is not 0", "0", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH)); assertNull("Accept-Ranges should not be set", restResponseChannel.getHeader(RestUtils.Headers.ACCEPT_RANGES)); assertNull("Content-Range header should not be set", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); verifyBlobProperties(expectedHeaders, restResponseChannel); verifyUserMetadataHeaders(expectedHeaders, restResponseChannel); } /** * 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 optional {@link ByteRange} for the request. * @param expectedHeaders the expected headers in the response. * @throws Exception */ private void getHeadAndVerify(String blobId, ByteRange range, JSONObject expectedHeaders) throws Exception { RestRequest restRequest = createRestRequest(RestMethod.HEAD, blobId, createRequestHeaders(range), null); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", range == null ? ResponseStatus.Ok : ResponseStatus.PartialContent, restResponseChannel.getStatus()); checkCommonGetHeadHeaders(restResponseChannel); assertEquals(RestUtils.Headers.CONTENT_TYPE + " does not match " + RestUtils.Headers.AMBRY_CONTENT_TYPE, expectedHeaders.getString(RestUtils.Headers.AMBRY_CONTENT_TYPE), restResponseChannel.getHeader(RestUtils.Headers.CONTENT_TYPE)); assertEquals("Accept-Ranges not set correctly", "bytes", restResponseChannel.getHeader(RestUtils.Headers.ACCEPT_RANGES)); long contentLength = expectedHeaders.getLong(RestUtils.Headers.BLOB_SIZE); if (range != null) { Pair<String, Long> rangeAndLength = RestUtils.buildContentRangeAndLength(range, contentLength); assertEquals("Content-Range does not match expected", rangeAndLength.getFirst(), restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); contentLength = rangeAndLength.getSecond(); } else { assertNull("Content-Range header should not be set", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_RANGE)); } assertEquals(RestUtils.Headers.CONTENT_LENGTH + " does not match expected", Long.toString(contentLength), restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH)); verifyBlobProperties(expectedHeaders, restResponseChannel); } /** * Verifies blob properties from output, to that sent in during input * @param expectedHeaders the expected headers in the response. * @param restResponseChannel the {@link RestResponseChannel} which contains the response. * @throws JSONException */ private void verifyBlobProperties(JSONObject expectedHeaders, MockRestResponseChannel restResponseChannel) throws JSONException { assertEquals(RestUtils.Headers.BLOB_SIZE + " does not match", expectedHeaders.getString(RestUtils.Headers.BLOB_SIZE), restResponseChannel.getHeader(RestUtils.Headers.BLOB_SIZE)); assertEquals(RestUtils.Headers.SERVICE_ID + " does not match", expectedHeaders.getString(RestUtils.Headers.SERVICE_ID), restResponseChannel.getHeader(RestUtils.Headers.SERVICE_ID)); assertEquals(RestUtils.Headers.PRIVATE + " does not match", expectedHeaders.getString(RestUtils.Headers.PRIVATE), restResponseChannel.getHeader(RestUtils.Headers.PRIVATE)); assertEquals(RestUtils.Headers.AMBRY_CONTENT_TYPE + " does not match", expectedHeaders.getString(RestUtils.Headers.AMBRY_CONTENT_TYPE), restResponseChannel.getHeader(RestUtils.Headers.AMBRY_CONTENT_TYPE)); assertTrue(RestUtils.Headers.CREATION_TIME + " header missing", restResponseChannel.getHeader(RestUtils.Headers.CREATION_TIME) != null); if (expectedHeaders.getLong(RestUtils.Headers.TTL) != Utils.Infinite_Time) { assertEquals(RestUtils.Headers.TTL + " does not match", expectedHeaders.getString(RestUtils.Headers.TTL), restResponseChannel.getHeader(RestUtils.Headers.TTL)); } if (expectedHeaders.has(RestUtils.Headers.OWNER_ID)) { assertEquals(RestUtils.Headers.OWNER_ID + " does not match", expectedHeaders.getString(RestUtils.Headers.OWNER_ID), restResponseChannel.getHeader(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 restResponseChannel the {@link RestResponseChannel} which contains the response. * @throws JSONException */ private void verifyUserMetadataHeaders(JSONObject expectedHeaders, MockRestResponseChannel restResponseChannel) throws JSONException { Iterator itr = expectedHeaders.keys(); while (itr.hasNext()) { String key = (String) itr.next(); if (key.startsWith(RestUtils.Headers.USER_META_DATA_HEADER_PREFIX)) { String outValue = restResponseChannel.getHeader(key); assertEquals("Value for " + key + " does not match in user metadata", expectedHeaders.getString(key), outValue); } } } /** * Deletes the blob with blob ID {@code blobId} and verifies the response returned. * @param blobId the blob ID of the blob to DELETE. * @throws Exception */ private void deleteBlobAndVerify(String blobId) throws Exception { RestRequest restRequest = createRestRequest(RestMethod.DELETE, blobId, null, null); verifyDeleteAccepted(restRequest); } /** * Verifies that the right {@link ResponseStatus} is returned for GET, HEAD and DELETE once a blob is deleted. * @param blobId the ID of the blob that was deleted. * @throws Exception */ private void verifyOperationsAfterDelete(String blobId) throws Exception { RestRequest restRequest = createRestRequest(RestMethod.GET, blobId, null, null); verifyGone(restRequest); restRequest = createRestRequest(RestMethod.HEAD, blobId, null, null); verifyGone(restRequest); restRequest = createRestRequest(RestMethod.DELETE, blobId, null, null); verifyDeleteAccepted(restRequest); } /** * Verifies that a blob is GONE after it is deleted. * @param restRequest the {@link RestRequest} to send to {@link AmbryBlobStorageService}. */ private void verifyGone(RestRequest restRequest) throws Exception { MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); try { doOperation(restRequest, restResponseChannel); fail("Operation should have failed because blob is deleted"); } catch (RestServiceException e) { assertEquals("AmbryBlobStorageService should have thrown a Deleted exception", RestServiceErrorCode.Deleted, e.getErrorCode()); } } /** * Verifies that a request returns the right response code once the blob has been deleted. * @param restRequest the {@link RestRequest} to send to {@link AmbryBlobStorageService}. * @throws Exception */ private void verifyDeleteAccepted(RestRequest restRequest) throws Exception { MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); doOperation(restRequest, restResponseChannel); assertEquals("Unexpected response status", ResponseStatus.Accepted, restResponseChannel.getStatus()); assertTrue("No Date header", restResponseChannel.getHeader(RestUtils.Headers.DATE) != null); assertEquals("Content-Length is not 0", "0", restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH)); } /** * Checks headers that are common to HEAD and GET. * @param restResponseChannel the {@link RestResponseChannel} to check headers on. */ private void checkCommonGetHeadHeaders(MockRestResponseChannel restResponseChannel) { assertTrue("No Date header", restResponseChannel.getHeader(RestUtils.Headers.DATE) != null); assertTrue("No Last-Modified header", restResponseChannel.getHeader("Last-Modified") != null); } // IdConverter and SecurityService exception testing helpers. /** * Does the exception pipelining test for {@link IdConverter}. * @param converterFactory the {@link IdConverterFactory} to use to while creating {@link AmbryBlobStorageService}. * @param expectedExceptionMsg the expected exception message. * @throws InstantiationException * @throws JSONException */ private void doIdConverterExceptionTest(FrontendTestIdConverterFactory converterFactory, String expectedExceptionMsg) throws InstantiationException, JSONException { ambryBlobStorageService = new AmbryBlobStorageService(frontendConfig, frontendMetrics, responseHandler, router, converterFactory, securityServiceFactory); ambryBlobStorageService.start(); doExternalServicesBadInputTest(RestMethod.values(), expectedExceptionMsg); } /** * Does the exception pipelining test for {@link SecurityService}. * @param securityFactory the {@link SecurityServiceFactory} to use to while creating {@link AmbryBlobStorageService}. * @param exceptionMsg the expected exception message. * @throws InstantiationException * @throws JSONException */ private void doSecurityServiceExceptionTest(FrontendTestSecurityServiceFactory securityFactory, String exceptionMsg) throws InstantiationException, JSONException { for (FrontendTestSecurityServiceFactory.Mode mode : FrontendTestSecurityServiceFactory.Mode.values()) { securityFactory.mode = mode; RestMethod[] restMethods; if (mode.equals(FrontendTestSecurityServiceFactory.Mode.Request)) { restMethods = RestMethod.values(); } else { restMethods = new RestMethod[3]; restMethods[0] = RestMethod.GET; restMethods[1] = RestMethod.HEAD; restMethods[2] = RestMethod.POST; } ambryBlobStorageService = new AmbryBlobStorageService(frontendConfig, frontendMetrics, responseHandler, new FrontendTestRouter(), idConverterFactory, securityFactory); ambryBlobStorageService.start(); doExternalServicesBadInputTest(restMethods, exceptionMsg); } } /** * Does the tests to check for exception pipelining for exceptions returned/thrown by external services. * @param restMethods the {@link RestMethod} types for which the test has to be run. * @param expectedExceptionMsg the expected exception message. * @throws JSONException */ private void doExternalServicesBadInputTest(RestMethod[] restMethods, String expectedExceptionMsg) throws JSONException { for (RestMethod restMethod : restMethods) { if (restMethod.equals(RestMethod.UNKNOWN) || restMethod.equals(RestMethod.PUT)) { continue; } JSONObject headers = new JSONObject(); List<ByteBuffer> contents = null; if (restMethod.equals(RestMethod.POST)) { setAmbryHeadersForPut(headers, 7200, false, "doExternalServicesBadInputTest", "application/octet-stream", "doExternalServicesBadInputTest"); contents = new ArrayList<ByteBuffer>(1); contents.add(null); } try { doOperation(createRestRequest(restMethod, "/", headers, contents), new MockRestResponseChannel()); fail("Operation " + restMethod + " should have failed because an external service would have thrown an exception"); } catch (Exception e) { assertEquals("Unexpected exception message", expectedExceptionMsg, e.getMessage()); } } } // routerExceptionPipelineTest() helpers. /** * Does the exception pipelining test for {@link Router}. * @param testRouter the {@link Router} to use to while creating {@link AmbryBlobStorageService}. * @param exceptionMsg the expected exception message. * @throws Exception */ private void doRouterExceptionPipelineTest(FrontendTestRouter testRouter, String exceptionMsg) throws Exception { ambryBlobStorageService = new AmbryBlobStorageService(frontendConfig, frontendMetrics, responseHandler, testRouter, idConverterFactory, securityServiceFactory); ambryBlobStorageService.start(); for (RestMethod restMethod : RestMethod.values()) { switch (restMethod) { case HEAD: testRouter.exceptionOpType = FrontendTestRouter.OpType.GetBlob; checkRouterExceptionPipeline(exceptionMsg, createRestRequest(restMethod, "/", null, null)); break; case GET: testRouter.exceptionOpType = FrontendTestRouter.OpType.GetBlob; checkRouterExceptionPipeline(exceptionMsg, createRestRequest(restMethod, "/", null, null)); break; case POST: testRouter.exceptionOpType = FrontendTestRouter.OpType.PutBlob; JSONObject headers = new JSONObject(); setAmbryHeadersForPut(headers, 7200, false, "routerExceptionPipelineTest", "application/octet-stream", "routerExceptionPipelineTest"); checkRouterExceptionPipeline(exceptionMsg, createRestRequest(restMethod, "/", headers, null)); break; case DELETE: testRouter.exceptionOpType = FrontendTestRouter.OpType.DeleteBlob; checkRouterExceptionPipeline(exceptionMsg, createRestRequest(restMethod, "/", null, null)); break; default: break; } } } /** * Checks that the exception received by submitting {@code restRequest} to {@link AmbryBlobStorageService} matches * what was expected. * @param expectedExceptionMsg the expected exception message. * @param restRequest the {@link RestRequest} to submit to {@link AmbryBlobStorageService}. * @throws Exception */ private void checkRouterExceptionPipeline(String expectedExceptionMsg, RestRequest restRequest) throws Exception { try { doOperation(restRequest, new MockRestResponseChannel()); fail("Operation " + restRequest.getRestMethod() + " should have failed because an external service would have thrown an exception"); } catch (RestServiceException | RuntimeException e) { // catching RestServiceException because RouterException should have been converted. // RuntimeException might get bubbled up as is. assertEquals("Unexpected exception message", expectedExceptionMsg, Utils.getRootCause(e).getMessage()); // Nothing should be closed. assertTrue("RestRequest channel is not open", restRequest.isOpen()); restRequest.close(); } } /** * Generate a {@link JSONObject} with a range header from a {@link ByteRange} * @param range the {@link ByteRange} to include in the headers. * @return the {@link JSONObject} with a range header, or null if {@code range} is null. * @throws Exception */ private JSONObject createRequestHeaders(ByteRange range) throws Exception { if (range == null) { return null; } JSONObject requestHeaders = new JSONObject(); requestHeaders.put(RestUtils.Headers.RANGE, RestTestUtils.getRangeHeaderString(range)); return requestHeaders; } } /** * An implementation of {@link RestResponseHandler} that stores a submitted response/exception and signals the fact * that the response has been submitted. A single instance can handle only a single response at a time. To reuse, call * {@link #reset()}. */ class FrontendTestResponseHandler implements RestResponseHandler { private volatile CountDownLatch responseSubmitted = new CountDownLatch(1); private volatile ReadableStreamChannel response = null; private volatile Exception exception = null; private volatile boolean serviceRunning = false; @Override public void start() { serviceRunning = true; } @Override public void shutdown() { serviceRunning = false; } @Override public void handleResponse(RestRequest restRequest, RestResponseChannel restResponseChannel, ReadableStreamChannel response, Exception exception) throws RestServiceException { if (serviceRunning) { this.response = response; this.exception = exception; if (response != null && exception == null) { try { response.readInto(restResponseChannel, null).get(); } catch (Exception e) { this.exception = e; } } restResponseChannel.onResponseComplete(exception); responseSubmitted.countDown(); } else { throw new RestServiceException("Response handler inactive", RestServiceErrorCode.RequestResponseQueuingFailure); } } /** * Wait for response to be submitted. * @param timeout the length of time to wait for. * @param timeUnit the time unit of {@code timeout}. * @return {@code true} if response was submitted within {@code timeout}. {@code false} otherwise. * @throws InterruptedException */ public boolean awaitResponseSubmission(long timeout, TimeUnit timeUnit) throws InterruptedException { return responseSubmitted.await(timeout, timeUnit); } /** * Gets the exception that was submitted, if any. Returns null if queried before response is submitted. * @return exception that that was submitted, if any. */ public Exception getException() { return exception; } /** * Gets the response that was submitted, if any. Returns null if queried before response is submitted. * @return response that that was submitted as a {@link ReadableStreamChannel}. */ public ReadableStreamChannel getResponse() { return response; } /** * Resets state so that this instance can be reused. */ public void reset() { response = null; exception = null; responseSubmitted = new CountDownLatch(1); } } /** * Implementation of {@link SecurityServiceFactory} that returns exceptions. */ class FrontendTestSecurityServiceFactory implements SecurityServiceFactory { /** * Defines the API in which {@link #exceptionToThrow} and {@link #exceptionToReturn} will work. */ protected enum Mode { /** * Works in {@link SecurityService#processRequest(RestRequest, Callback)}. */ Request, /** * Works in {@link SecurityService#processResponse(RestRequest, RestResponseChannel, BlobInfo, Callback)}. */ Response } /** * The exception to return via future/callback. */ public Exception exceptionToReturn = null; /** * The exception to throw on function invocation. */ public RuntimeException exceptionToThrow = null; /** * Defines the API in which {@link #exceptionToThrow} and {@link #exceptionToReturn} will work. */ public Mode mode = Mode.Request; @Override public SecurityService getSecurityService() { return new TestSecurityService(); } private class TestSecurityService implements SecurityService { private boolean isOpen = true; @Override public Future<Void> processRequest(RestRequest restRequest, Callback<Void> callback) { if (!isOpen) { throw new IllegalStateException("SecurityService closed"); } return completeOperation(callback, mode == null || mode == Mode.Request); } @Override public Future<Void> processResponse(RestRequest restRequest, RestResponseChannel responseChannel, BlobInfo blobInfo, Callback<Void> callback) { if (!isOpen) { throw new IllegalStateException("SecurityService closed"); } return completeOperation(callback, mode == Mode.Response); } @Override public void close() { isOpen = false; } /** * Completes the operation by creating and invoking a {@link Future} and invoking the {@code callback} if non-null. * @param callback the {@link Callback} to invoke. Can be null. * @param misbehaveIfRequired whether to exhibit misbehavior or not. * @return the created {@link Future}. */ private Future<Void> completeOperation(Callback<Void> callback, boolean misbehaveIfRequired) { if (misbehaveIfRequired && exceptionToThrow != null) { throw exceptionToThrow; } FutureResult<Void> futureResult = new FutureResult<Void>(); futureResult.done(null, misbehaveIfRequired ? exceptionToReturn : null); if (callback != null) { callback.onCompletion(null, misbehaveIfRequired ? exceptionToReturn : null); } return futureResult; } } } /** * Implementation of {@link IdConverterFactory} that returns exceptions. */ class FrontendTestIdConverterFactory implements IdConverterFactory { public Exception exceptionToReturn = null; public RuntimeException exceptionToThrow = null; @Override public IdConverter getIdConverter() { return new TestIdConverter(); } private class TestIdConverter implements IdConverter { private boolean isOpen = true; @Override public Future<String> convert(RestRequest restRequest, String input, Callback<String> callback) { if (!isOpen) { throw new IllegalStateException("IdConverter closed"); } return completeOperation(callback); } @Override public void close() { isOpen = false; } /** * Completes the operation by creating and invoking a {@link Future} and invoking the {@code callback} if non-null. * @param callback the {@link Callback} to invoke. Can be null. * @return the created {@link Future}. */ private Future<String> completeOperation(Callback<String> callback) { if (exceptionToThrow != null) { throw exceptionToThrow; } FutureResult<String> futureResult = new FutureResult<String>(); futureResult.done(null, exceptionToReturn); if (callback != null) { callback.onCompletion(null, exceptionToReturn); } return futureResult; } } } /** * A bad implementation of {@link RestRequest}. Just throws exceptions. */ class BadRestRequest extends BadRSC implements RestRequest { @Override public RestMethod getRestMethod() { return null; } @Override public String getPath() { throw new IllegalStateException("Not implemented"); } @Override public String getUri() { return null; } @Override public Map<String, Object> getArgs() { throw new IllegalStateException("Not implemented"); } @Override public SSLSession getSSLSession() { return null; } @Override public void prepare() { throw new IllegalStateException("Not implemented"); } @Override public RestRequestMetricsTracker getMetricsTracker() { return new RestRequestMetricsTracker(); } @Override public void setDigestAlgorithm(String digestAlgorithm) { throw new IllegalStateException("Not implemented"); } @Override public byte[] getDigest() { throw new IllegalStateException("Not implemented"); } } /** * A bad implementation of {@link ReadableStreamChannel}. Just throws exceptions. */ class BadRSC implements ReadableStreamChannel { @Override public long getSize() { return -1; } @Override public Future<Long> readInto(AsyncWritableChannel asyncWritableChannel, Callback<Long> callback) { throw new IllegalStateException("Not implemented"); } @Override public boolean isOpen() { throw new IllegalStateException("Not implemented"); } @Override public void close() throws IOException { throw new IOException("Not implemented"); } } /** * Implementation of {@link Router} that responds immediately or throws exceptions as required. */ class FrontendTestRouter implements Router { private boolean isOpen = true; /** * Enumerates the different operation types in the router. */ enum OpType { DeleteBlob, GetBlob, PutBlob } public OpType exceptionOpType = null; public Exception exceptionToReturn = null; public RuntimeException exceptionToThrow = null; public String deleteServiceId = null; @Override public Future<GetBlobResult> getBlob(String blobId, GetBlobOptions options) { return getBlob(blobId, options, null); } @Override public Future<GetBlobResult> getBlob(String blobId, GetBlobOptions options, Callback<GetBlobResult> callback) { GetBlobResult result; switch (options.getOperationType()) { case BlobInfo: result = new GetBlobResult(new BlobInfo(new BlobProperties(0, "FrontendTestRouter"), new byte[0]), null); break; case Data: result = new GetBlobResult(null, new ByteBufferReadableStreamChannel(ByteBuffer.allocate(0))); break; default: result = new GetBlobResult(new BlobInfo(new BlobProperties(0, "FrontendTestRouter"), new byte[0]), new ByteBufferReadableStreamChannel(ByteBuffer.allocate(0))); break; } return completeOperation(result, callback, OpType.GetBlob); } @Override public Future<String> putBlob(BlobProperties blobProperties, byte[] usermetadata, ReadableStreamChannel channel) { return putBlob(blobProperties, usermetadata, channel, null); } @Override public Future<String> putBlob(BlobProperties blobProperties, byte[] usermetadata, ReadableStreamChannel channel, Callback<String> callback) { return completeOperation(UtilsTest.getRandomString(10), callback, OpType.PutBlob); } @Override public Future<Void> deleteBlob(String blobId, String serviceId) { return deleteBlob(blobId, serviceId, null); } @Override public Future<Void> deleteBlob(String blobId, String serviceId, Callback<Void> callback) { deleteServiceId = serviceId; return completeOperation(null, callback, OpType.DeleteBlob); } @Override public void close() { isOpen = false; } /** * Completes the operation by creating and invoking a {@link Future} and invoking the {@code callback} if non-null. * @param result the result to return. * @param callback the {@link Callback} to invoke. Can be null. * @param opType the type of operation calling this function. * @param <T> the type of future/callback. * @return the created {@link Future}. */ private <T> Future<T> completeOperation(T result, Callback<T> callback, OpType opType) { if (!isOpen) { throw new IllegalStateException("Router not open"); } Exception exception = null; if (opType == exceptionOpType) { if (exceptionToThrow != null) { throw new RuntimeException(exceptionToThrow); } else if (exceptionToReturn != null) { exception = exceptionToReturn; result = null; } } FutureResult<T> futureResult = new FutureResult<T>(); futureResult.done(result, exception); if (callback != null) { callback.onCompletion(result, exception); } return futureResult; } }