/** * Copyright 2015 Google Inc. 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. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.apphosting.vmruntime; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.appengine.api.datastore.DatastoreFailureException; import com.google.appengine.api.log.LogServiceException; import com.google.appengine.api.modules.ModulesException; import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.ApiConfig; import com.google.apphosting.api.ApiProxy.ApiProxyException; import com.google.apphosting.utils.remoteapi.RemoteApiPb; import com.google.appengine.repackaged.com.google.common.collect.ImmutableMap; import junit.framework.TestCase; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.protocol.HttpContext; import org.mockito.Mockito; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; /** * Tests the delegate for making AppEngine API calls in a Google Compute Engine VM. * */ public class VmApiProxyDelegateTest extends TestCase { private static final String TICKET = "test-ticket"; public static final String TEST_PACKAGE_NAME = "test package"; public static final String TEST_METHOD_NAME = "test method"; public static final String TEST_ERROR_MESSAGE = "test error message"; public static final int TEST_APPLICATION_ERROR = 505; public static Map<RemoteApiPb.RpcError.ErrorCode, ApiProxyException> createErrorToExceptionMap() { return new ImmutableMap.Builder<RemoteApiPb.RpcError.ErrorCode, ApiProxyException>() .put(RemoteApiPb.RpcError.ErrorCode.UNKNOWN, new ApiProxy.UnknownException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.CALL_NOT_FOUND, new ApiProxy.CallNotFoundException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.PARSE_ERROR, new ApiProxy.ArgumentException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.SECURITY_VIOLATION, new ApiProxy.UnknownException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.OVER_QUOTA, new ApiProxy.OverQuotaException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.REQUEST_TOO_LARGE, new ApiProxy.RequestTooLargeException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put( RemoteApiPb.RpcError.ErrorCode.CAPABILITY_DISABLED, new ApiProxy.CapabilityDisabledException(TEST_ERROR_MESSAGE, TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put( RemoteApiPb.RpcError.ErrorCode.FEATURE_DISABLED, new ApiProxy.FeatureNotEnabledException(TEST_ERROR_MESSAGE, TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.BAD_REQUEST, new ApiProxy.ArgumentException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.RESPONSE_TOO_LARGE, new ApiProxy.ResponseTooLargeException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.CANCELLED, new ApiProxy.CancelledException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.REPLAY_ERROR, new ApiProxy.UnknownException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .put(RemoteApiPb.RpcError.ErrorCode.DEADLINE_EXCEEDED, new ApiProxy.ApiDeadlineExceededException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)) .build(); } private HttpClient createMockHttpClient() { HttpClient httpClient = mock(HttpClient.class); when(httpClient.getConnectionManager()).thenReturn(new PoolingClientConnectionManager()); return httpClient; } private HttpResponse createMockHttpResponse(byte[] response, int code) throws IllegalStateException, IOException { ByteArrayInputStream istream = new ByteArrayInputStream(response); HttpResponse resp = mock(HttpResponse.class); HttpEntity entity = mock(HttpEntity.class); StatusLine statusLine = mock(StatusLine.class); when(entity.getContent()).thenReturn(istream); when(resp.getEntity()).thenReturn(entity); when(statusLine.getStatusCode()).thenReturn(code); when(resp.getStatusLine()).thenReturn(statusLine); return resp; } private VmApiProxyEnvironment createMockEnvironment() { VmApiProxyEnvironment environment = mock(VmApiProxyEnvironment.class); when(environment.getTicket()).thenReturn(TICKET); Map<String, Object> attributes = new HashMap<>(); when(environment.getAttributes()).thenReturn(attributes); return environment; } private void callDelegateWithSuccess(boolean sync) throws Exception { RemoteApiPb.Response response = new RemoteApiPb.Response(); byte[] pbData = new byte[] {0, 1, 2, 3, 4, 5}; response.setResponseAsBytes(pbData); HttpClient mockClient = createMockHttpClient(); HttpResponse mockHttpResponse = createMockHttpResponse(response.toByteArray(), HttpURLConnection.HTTP_OK); when(mockClient.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) .thenReturn(mockHttpResponse); VmApiProxyDelegate delegate = new VmApiProxyDelegate(mockClient); VmApiProxyEnvironment environment = createMockEnvironment(); final Double timeoutInSeconds = 3.0; byte[] result = null; if (sync) { environment.getAttributes().put(VmApiProxyDelegate.API_DEADLINE_KEY, timeoutInSeconds); result = delegate.makeSyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, pbData); } else { ApiConfig apiConfig = new ApiConfig(); apiConfig.setDeadlineInSeconds(timeoutInSeconds); result = delegate.makeAsyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, pbData, apiConfig).get(); } assertTrue(Arrays.equals(pbData, result)); } private void callDelegateWithOneError(boolean sync, RemoteApiPb.RpcError rpcError, RemoteApiPb.ApplicationError appError, ApiProxyException expectedException) throws Exception { // Create the response for the mock connection. RemoteApiPb.Response response = new RemoteApiPb.Response(); if (appError != null) { response.setApplicationError(appError); } if (rpcError != null) { response.setRpcError(rpcError); } HttpClient mockClient = createMockHttpClient(); HttpResponse mockHttpResponse = createMockHttpResponse(response.toByteArray(), HttpURLConnection.HTTP_OK); when(mockClient.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) .thenReturn(mockHttpResponse); VmApiProxyDelegate delegate = new VmApiProxyDelegate(mockClient); VmApiProxyEnvironment environment = createMockEnvironment(); byte[] requestData = new byte[] {0, 1, 2, 3, 4, 5}; byte[] result = null; final Double timeoutInSeconds = 10.0; if (sync) { try { environment.getAttributes().put(VmApiProxyDelegate.API_DEADLINE_KEY, timeoutInSeconds); result = delegate.makeSyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData); fail(); } catch (ApiProxyException exception) { assertEquals(expectedException.getClass(), exception.getClass()); } } else { try { ApiConfig apiConfig = new ApiConfig(); apiConfig.setDeadlineInSeconds(timeoutInSeconds); result = delegate.makeAsyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData, apiConfig).get(); fail(); } catch (ExecutionException exception) { // ExecutionException is expected, and make sure the cause is expected as well. assertEquals(expectedException.getClass(), exception.getCause().getClass()); } } assertNull(result); } private void callDelegateWithHttpError(boolean sync, ApiProxyException expectedException) throws Exception { HttpClient mockClient = createMockHttpClient(); HttpResponse mockHttpResponse = createMockHttpResponse("Error from RPC proxy".getBytes(), HttpURLConnection.HTTP_UNAVAILABLE); when(mockClient.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) .thenReturn(mockHttpResponse); VmApiProxyDelegate delegate = new VmApiProxyDelegate(mockClient); VmApiProxyEnvironment environment = createMockEnvironment(); byte[] requestData = new byte[] {0, 1, 2, 3, 4, 5}; byte[] result = null; final Double timeoutInSeconds = 10.0; if (sync) { try { environment.getAttributes().put(VmApiProxyDelegate.API_DEADLINE_KEY, timeoutInSeconds); result = delegate.makeSyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData); fail(); } catch (ApiProxyException exception) { assertEquals(expectedException.getClass(), exception.getClass()); } } else { try { ApiConfig apiConfig = new ApiConfig(); apiConfig.setDeadlineInSeconds(timeoutInSeconds); result = delegate.makeAsyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData, apiConfig).get(); fail(); } catch (ExecutionException exception) { // ExecutionException is expected, and make sure the cause is expected as well. assertEquals(expectedException.getClass(), exception.getCause().getClass()); } } assertNull(result); } private void callDelegateWithConnectionError(boolean sync, ApiProxyException expectedException) throws Exception { HttpClient mockClient = createMockHttpClient(); createMockHttpResponse(new byte[0], HttpURLConnection.HTTP_UNAVAILABLE); when(mockClient.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) .thenThrow(new IOException("Connection refused")); VmApiProxyDelegate delegate = new VmApiProxyDelegate(mockClient); VmApiProxyEnvironment environment = createMockEnvironment(); byte[] requestData = new byte[] {0, 1, 2, 3, 4, 5}; byte[] result = null; final Double timeoutInSeconds = 10.0; if (sync) { try { environment.getAttributes().put(VmApiProxyDelegate.API_DEADLINE_KEY, timeoutInSeconds); result = delegate.makeSyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData); fail(); } catch (ApiProxyException exception) { assertEquals(expectedException.getClass(), exception.getClass()); } } else { try { ApiConfig apiConfig = new ApiConfig(); apiConfig.setDeadlineInSeconds(timeoutInSeconds); result = delegate.makeAsyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData, apiConfig).get(); fail(); } catch (ExecutionException exception) { // ExecutionException is expected, and make sure the cause is expected as well. assertEquals(expectedException.getClass(), exception.getCause().getClass()); } } assertNull(result); } private void callDelegateWithParsingError(boolean sync, ApiProxyException expectedException) throws Exception { // Create the response for the mock connection. byte[] brokenResponse = new byte[] {47, 11, 17, 32}; HttpClient mockClient = createMockHttpClient(); HttpResponse mockHttpResponse = createMockHttpResponse(brokenResponse, HttpURLConnection.HTTP_OK); when(mockClient.execute(Mockito.any(HttpUriRequest.class), Mockito.any(HttpContext.class))) .thenReturn(mockHttpResponse); VmApiProxyDelegate delegate = new VmApiProxyDelegate(mockClient); VmApiProxyEnvironment environment = createMockEnvironment(); byte[] requestData = new byte[] {0, 1, 2, 3, 4, 5}; byte[] result = null; if (sync) { try { result = delegate.makeSyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData); fail(); } catch (ApiProxyException exception) { assertEquals(expectedException.getClass(), exception.getClass()); } } else { try { result = delegate.makeAsyncCall(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, requestData, new ApiConfig()).get(); fail(); } catch (ExecutionException exception) { // ExecutionException is expected, and make sure the cause is expected as well. assertEquals(expectedException.getClass(), exception.getCause().getClass()); } } assertNull(result); } private void callDelegateWithAllErrors(boolean sync) throws Exception { Map<RemoteApiPb.RpcError.ErrorCode, ApiProxyException> rmtResponseRpcErrorToExceptionMap = createErrorToExceptionMap(); for (Map.Entry<RemoteApiPb.RpcError.ErrorCode, ApiProxyException> entry : rmtResponseRpcErrorToExceptionMap.entrySet()) { RemoteApiPb.RpcError rpcError = new RemoteApiPb.RpcError(); rpcError.setCode(entry.getKey().ordinal()); ApiProxyException expectedException = entry.getValue(); callDelegateWithOneError(sync, rpcError, null, expectedException); } RemoteApiPb.ApplicationError appError = new RemoteApiPb.ApplicationError(); appError.setCode(13); appError.setDetail("blah"); ApiProxyException expectedException = new ApiProxy.ApplicationException(TEST_APPLICATION_ERROR, TEST_ERROR_MESSAGE); callDelegateWithOneError(sync, null, appError, expectedException); } public void testConstructorAndConnectionMonitorThread() throws Exception { VmApiProxyDelegate delegate = new VmApiProxyDelegate(); delegate.monitorThread.interrupt(); delegate.monitorThread.join(); } public void testMakeSyncCall_Success() throws Exception { callDelegateWithSuccess(true); } public void testMakeAsyncCall_Success() throws Exception { callDelegateWithSuccess(false); } public void testMakeSyncCall_HttpError() throws Exception { callDelegateWithHttpError(true, new ApiProxy.RPCFailedException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)); } public void testMakeAsyncCall_HttpError() throws Exception { callDelegateWithHttpError(false, new ApiProxy.RPCFailedException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)); } public void testMakeSyncCall_ConnectionError() throws Exception { callDelegateWithConnectionError(true, new ApiProxy.RPCFailedException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)); } public void testMakeAsyncCall_ConnectionError() throws Exception { callDelegateWithConnectionError(false, new ApiProxy.RPCFailedException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)); } public void testMakeSyncCall_ParsingError() throws Exception { callDelegateWithParsingError(true, new ApiProxy.RPCFailedException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)); } public void testMakeAsyncCall_ParsingError() throws Exception { callDelegateWithParsingError(false, new ApiProxy.RPCFailedException(TEST_PACKAGE_NAME, TEST_METHOD_NAME)); } public void testMakeSyncCall_AllErrors() throws Exception { callDelegateWithAllErrors(true); } public void testMakeAsyncCall_AllErrors() throws Exception { callDelegateWithAllErrors(false); } public void testUnknownRpcError() throws Exception { RemoteApiPb.RpcError rpcError = new RemoteApiPb.RpcError(); rpcError.setCode(123456); ApiProxyException expectedException = new ApiProxy.UnknownException(TEST_PACKAGE_NAME, TEST_METHOD_NAME); callDelegateWithOneError(false, rpcError, null, expectedException); } public void testCreateRequest() throws Exception { VmApiProxyEnvironment environment = createMockEnvironment(); environment.getAttributes().put(VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.attributeKey, "abc123"); // assertFalse(environment.getForceReuseApiConnection()); int timeoutMs = 17 * 1000; byte[] apiRequestData = new byte[] {1, 2, 3, 4}; HttpPost request = VmApiProxyDelegate.createRequest(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, apiRequestData, timeoutMs); assertEquals(request.getFirstHeader(VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.headerKey) .getValue(), "abc123"); assertEquals(request.getEntity().getContentType().getValue(), "application/octet-stream"); assertEquals(request.getFirstHeader(VmApiProxyDelegate.RPC_STUB_ID_HEADER).getValue(), VmApiProxyDelegate.REQUEST_STUB_ID); assertEquals(request.getFirstHeader(VmApiProxyDelegate.RPC_METHOD_HEADER).getValue(), VmApiProxyDelegate.REQUEST_STUB_METHOD); assertEquals(request.getFirstHeader(VmApiProxyDelegate.RPC_DEADLINE_HEADER).getValue(), Double.toString(timeoutMs / 1000)); // Disable keep-alive, workaround for b/. // TODO(b/): revert when the underlying TCP retransmission bug is fixed. // assertEquals(request.getFirstHeader("Connection").getValue(), "close"); RemoteApiPb.Request rmtRequest = new RemoteApiPb.Request(); assertTrue(rmtRequest.parseFrom(request.getEntity().getContent())); assertEquals(TEST_PACKAGE_NAME, rmtRequest.getServiceName()); assertEquals(TEST_METHOD_NAME, rmtRequest.getMethod()); assertEquals(TICKET, rmtRequest.getRequestId()); assertTrue(Arrays.equals(apiRequestData, rmtRequest.getRequestAsBytes())); } /*public void testCreateRequest_ForceReuseApiConnection() throws Exception { // TODO(b/): delete when the underlying TCP retransmission bug is fixed. VmApiProxyEnvironment environment = createMockEnvironment(); when(environment.getForceReuseApiConnection()).thenReturn(true); assertTrue(environment.getForceReuseApiConnection()); int timeoutMs = 17 * 1000; byte[] apiRequestData = new byte[] {1, 2, 3, 4}; HttpPost request = VmApiProxyDelegate.createRequest(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, apiRequestData, timeoutMs); assertNull(request.getFirstHeader("Connection")); }*/ public void testCreateRequest_DapperHeaderForwarding() throws Exception { VmApiProxyEnvironment environment = createMockEnvironment(); environment.getAttributes().put(VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.attributeKey, "abc123"); HttpPost request = VmApiProxyDelegate.createRequest(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, new byte[0], 0); assertEquals(request.getFirstHeader(VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.headerKey) .getValue(), "abc123"); } public void testCreateRequest_DeadlineFromEnvironment() throws Exception { VmApiProxyEnvironment environment = createMockEnvironment(); final Double deadline = 10.0; environment.getAttributes().put(VmApiProxyDelegate.API_DEADLINE_KEY, deadline); HttpPost request = VmApiProxyDelegate.createRequest(environment, TEST_PACKAGE_NAME, TEST_METHOD_NAME, new byte[0], 0); assertEquals(request.getFirstHeader(VmApiProxyDelegate.RPC_DEADLINE_HEADER).getValue(), Double.toString(deadline)); } public void testAPIExceptionWrapping() { VmApiProxyDelegate delegate = new VmApiProxyDelegate(createMockHttpClient()); RuntimeException exception = delegate.constructApiException("logservice", "a"); assertEquals(LogServiceException.class, exception.getClass()); assertEquals("RCP Failure for API call: logservice a", exception.getMessage()); exception = delegate.constructApiException("modules", "b"); assertEquals(ModulesException.class, exception.getClass()); assertEquals("RCP Failure for API call: modules b", exception.getMessage()); exception = delegate.constructApiException("datastore_v3", "c"); assertEquals(DatastoreFailureException.class, exception.getClass()); assertEquals("RCP Failure for API call: datastore_v3 c", exception.getMessage()); exception = delegate.constructApiException("barf", "d"); assertEquals(ApiProxy.RPCFailedException.class, exception.getClass()); assertEquals( "The remote RPC to the application server failed for the call barf.d().", exception.getMessage()); } }