/* * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. 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://aws.amazon.com/apache2.0 * * This file 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.amazonaws.mobileconnectors.amazonmobileanalytics.internal.delivery; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.amazonaws.AmazonServiceException; import com.amazonaws.RequestClientOptions; import com.amazonaws.mobileconnectors.amazonmobileanalytics.MobileAnalyticsTestBase; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.core.AnalyticsContext; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.core.configuration.Configuration; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.delivery.EventStore.EventIterator; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.delivery.policy.DefaultDeliveryPolicyFactory; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.delivery.policy.DeliveryPolicy; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.event.InternalEvent; import com.amazonaws.mobileconnectors.amazonmobileanalytics.internal.event.adapter.EventAdapter; import com.amazonaws.mobileconnectors.amazonmobileanalytics.utils.AnalyticsContextBuilder; import com.amazonaws.services.mobileanalytics.AmazonMobileAnalyticsClient; import com.amazonaws.services.mobileanalytics.model.PutEventsRequest; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class DefaultDeliveryClientTest extends MobileAnalyticsTestBase { private static final String SDK_NAME = "AppIntelligenceSDK"; private static final String SDK_VERSION = "test"; private static final String UNIQUE_ID = "abc123"; public static final String EVENTS_DIR = "events"; private DefaultDeliveryClient target; ThreadPoolExecutor mockEventExecutor = Mockito.mock(ThreadPoolExecutor.class); ThreadPoolExecutor mockSubmissionExecutor = Mockito.mock(ThreadPoolExecutor.class); AmazonMobileAnalyticsClient mockErs = Mockito.mock(AmazonMobileAnalyticsClient.class); ERSRequestBuilder mockRequestBuilder = Mockito.mock(ERSRequestBuilder.class); Configuration mockConfig = Mockito.mock(Configuration.class); EventStore mockEventStore = Mockito.mock(EventStore.class); DefaultDeliveryPolicyFactory mockFactory = Mockito.mock(DefaultDeliveryPolicyFactory.class); PutEventsRequest mockRequest = Mockito.mock(PutEventsRequest.class); @SuppressWarnings("unchecked") EventAdapter<JSONObject> mockAdapter = (EventAdapter<JSONObject>) Mockito .mock(EventAdapter.class); @SuppressWarnings("unchecked") BlockingQueue<Runnable> mockEventQueue = (BlockingQueue<Runnable>) Mockito .mock(BlockingQueue.class); @SuppressWarnings("unchecked") BlockingQueue<Runnable> mockSubmissionQueue = (BlockingQueue<Runnable>) Mockito .mock(BlockingQueue.class); @SuppressWarnings("unchecked") @Before public void setup() { MockitoAnnotations.initMocks(this); AnalyticsContext mockContext = new AnalyticsContextBuilder() .withSdkInfo(SDK_NAME, SDK_VERSION) .withUniqueIdValue(UNIQUE_ID) .withERSClient(mockErs) .withConfiguration(mockConfig) .build(); when( mockConfig.optLong(DefaultDeliveryClient.KEY_MAX_SUBMISSION_SIZE, DefaultDeliveryClient.DEFAULT_MAX_SUBMISSION_SIZE)).thenReturn( DefaultDeliveryClient.DEFAULT_MAX_SUBMISSION_SIZE); when(mockEventQueue.remainingCapacity()).thenReturn(2); when(mockEventExecutor.getQueue()).thenReturn(mockEventQueue); when( mockConfig.optInt(DefaultDeliveryClient.KEY_MAX_SUBMISSIONS_ALLOWED, DefaultDeliveryClient.DEFAULT_MAX_SUBMISSIONS_ALLOWED)).thenReturn( DefaultDeliveryClient.DEFAULT_MAX_SUBMISSIONS_ALLOWED); when(mockRequest.getRequestClientOptions()).thenReturn(new RequestClientOptions()); when(mockRequestBuilder.createRecordEventsRequest(any(JSONArray.class), any(String.class))) .thenReturn(mockRequest); target = new DefaultDeliveryClient(mockContext, mockFactory, mockEventExecutor, mockSubmissionExecutor, mockRequestBuilder, mockEventStore, mockAdapter); } @Test public void enqueueEventForDelivery_writeMultipleEventsToDisk_eventsStoredInNewlineFile() throws JSONException { InternalEvent mockEvent = mock(InternalEvent.class); when(mockEvent.getEventType()).thenReturn("event_type"); // return the expected json anytime we use adapter to create json object JSONObject expectedJson = new JSONObject(); expectedJson.put("event_type", "event_type"); when(mockAdapter.translateFromEvent(mockEvent)).thenReturn(expectedJson); // enqueue an event 3 times target.enqueueEventForDelivery(mockEvent); target.enqueueEventForDelivery(mockEvent); target.enqueueEventForDelivery(mockEvent); ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mockEventExecutor, times(3)).execute(runnableCaptor.capture()); assertThat(runnableCaptor.getAllValues().size(), is(3)); for (Runnable runnable : runnableCaptor.getAllValues()) { runnable.run(); } // capture the strings that were written to the event store ArgumentCaptor<String> putEventCaptor = ArgumentCaptor.forClass(String.class); verify(mockEventStore, times(3)).put(putEventCaptor.capture()); assertThat(putEventCaptor.getAllValues().size(), is(3)); for (String eventString : putEventCaptor.getAllValues()) { assertThat(eventString, is(expectedJson.toString())); } } @Test public void attemptDelivery_verifyPayloadEventsDeleted() throws JSONException { final JSONObject expectedJson = new JSONObject(); expectedJson.put("event_type", "event_type"); // mock the file store to return 3 events via hasNext iterator calls EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true, true, true, true, false); when(mockIterator.peek()).thenReturn(expectedJson.toString()); when(mockIterator.next()).thenReturn(expectedJson.toString()); when(mockEventStore.iterator()).thenReturn(mockIterator); when(mockRequestBuilder.createRecordEventsRequest(any(JSONArray.class), any(String.class))) .thenAnswer( new Answer<PutEventsRequest>() { @Override public PutEventsRequest answer(InvocationOnMock invocation) throws Throwable { JSONArray requestArray = (JSONArray) invocation.getArguments()[0]; assertThat(requestArray.length(), is(3)); for (int i = 0; i < 3; i++) { assertThat(requestArray.get(i).toString(), is(expectedJson.toString())); } return mockRequest; } }); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockIterator, times(1)).removeReadEvents(); } @Test public void attemptDelivery_verifyBatchedPayloadEventsDeleted() throws JSONException { final JSONObject expectedJson = new JSONObject(); expectedJson.put("event_type", "event_type"); // mock the file store to return 3 events via hasNext iterator calls EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true, true, true, true, true, true, false); when(mockIterator.peek()).thenReturn(expectedJson.toString()); when(mockIterator.next()).thenReturn(expectedJson.toString()); when(mockEventStore.iterator()).thenReturn(mockIterator); // Set the maxSubmissionSize to only allow one event at a time so we // should get 3 requests with this maxSubmissionSize when( mockConfig.optLong(DefaultDeliveryClient.KEY_MAX_SUBMISSION_SIZE, DefaultDeliveryClient.DEFAULT_MAX_SUBMISSION_SIZE)).thenReturn(27L); // when(mockRequestBuilder.createRecordEventsRequest(any(JSONArray.class), any(String.class))) .thenAnswer( new Answer<PutEventsRequest>() { @Override public PutEventsRequest answer(InvocationOnMock invocation) throws Throwable { JSONArray requestArray = (JSONArray) invocation.getArguments()[0]; assertThat(requestArray.length(), is(1)); assertThat(requestArray.get(0).toString(), is(expectedJson.toString())); return mockRequest; } }); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockRequestBuilder, times(3)).createRecordEventsRequest(any(JSONArray.class), any(String.class)); verify(mockIterator, times(3)).removeReadEvents(); } @Test public void attemptDelivery_submissionTimePolicyPreventsSubmission() { setupMockPolicies(true, false); target.attemptDelivery(); verifyAndRunSubmissionExecutorService(1); verify(mockEventStore, times(0)).iterator(); } @Test public void attemptDelivery_connectivityTimePolicyPreventsSubmission() { setupMockPolicies(false, true); target.attemptDelivery(); verifyAndRunSubmissionExecutorService(1); verify(mockEventStore, times(0)).iterator(); } @Test public void attemptDelivery_policiesNotifiedAfterSuccessfulSubmission() { List<DeliveryPolicy> policies = setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verifiyPolicyList(policies, true); } @Test public void attemptDelivery_policiesNotifiedAfterRecoverableServerErrorResponse() { List<DeliveryPolicy> policies = setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw forbidden exception AmazonServiceException mockForbidden = Mockito.mock(AmazonServiceException.class); when(mockForbidden.getErrorCode()).thenReturn("AccessDenied"); Mockito.doThrow(mockForbidden).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verifiyPolicyList(policies, false); } @Test public void attemptDelivery_policiesNotifiedAfterNonRecoverableServerErrorResponse() { List<DeliveryPolicy> policies = setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw validation exception AmazonServiceException mockForbidden = Mockito.mock(AmazonServiceException.class); when(mockForbidden.getErrorCode()).thenReturn("ValidationException"); Mockito.doThrow(mockForbidden).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); // We consider this 'successful' because we delete the stored requests // and will not be retrying the request verifiyPolicyList(policies, true); } @Test public void attemptDelivery_ValidationResponseResultsInEventsDeleted() { setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw validation exception AmazonServiceException mockForbidden = Mockito.mock(AmazonServiceException.class); when(mockForbidden.getErrorCode()).thenReturn("ValidationException"); Mockito.doThrow(mockForbidden).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockIterator, times(1)).removeReadEvents(); } @Test public void attemptDelivery_BadRequestExceptionResponseResultsInEventsDeleted() { setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw validation exception AmazonServiceException mockForbidden = Mockito.mock(AmazonServiceException.class); when(mockForbidden.getErrorCode()).thenReturn("BadRequestException"); Mockito.doThrow(mockForbidden).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockIterator, times(1)).removeReadEvents(); } @Test public void attemptDelivery_SerializationExceptionResponseResultsInEventsDeleted() { setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw validation exception AmazonServiceException mockForbidden = Mockito.mock(AmazonServiceException.class); when(mockForbidden.getErrorCode()).thenReturn("SerializationException"); Mockito.doThrow(mockForbidden).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockIterator, times(1)).removeReadEvents(); } @Test public void attemptDelivery_RecoverableResponseResultsInEventsNotDeleted() { setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(true).thenReturn(false); when(mockIterator.peek()).thenReturn("{'event':'event'}").thenReturn("{'event':'event'}") .thenReturn(null); when(mockIterator.next()).thenReturn("{'event':'event'}").thenReturn(null); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw forbidden exception AmazonServiceException mockForbidden = Mockito.mock(AmazonServiceException.class); when(mockForbidden.getErrorCode()).thenReturn("AccessDenied"); Mockito.doThrow(mockForbidden).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockIterator, times(0)).removeReadEvents(); } @Test public void attemptDelivery_UnexpectedExceptionResponseResultsInEventsNotDeleted() { setupMockPolicies(true, true); EventIterator mockIterator = mock(EventIterator.class); when(mockIterator.hasNext()).thenReturn(false); when(mockEventStore.iterator()).thenReturn(mockIterator); // Make ERSClient throw I/O exception RuntimeException mockException = Mockito.mock(RuntimeException.class); Mockito.doThrow(mockException).when(mockErs).putEvents(any(PutEventsRequest.class)); target.attemptDelivery(); verifyAndRunEventsExecutorService(1); verifyAndRunSubmissionExecutorService(1); verify(mockIterator, times(0)).removeReadEvents(); } private List<DeliveryPolicy> setupMockPolicies(boolean connectivityAllowed, boolean submissionTimeAllowed) { List<DeliveryPolicy> policies = new ArrayList<DeliveryPolicy>(); policies.add(setupConnectivityPolicy(connectivityAllowed)); policies.add(setupForceSubmissionTimePolicy(submissionTimeAllowed)); return policies; } private static void verifiyPolicyList(List<DeliveryPolicy> policies, boolean expectedHandleDeliveryAttempt) { for (DeliveryPolicy policy : policies) { verify(policy, times(1)).handleDeliveryAttempt(expectedHandleDeliveryAttempt); } } private DeliveryPolicy setupConnectivityPolicy(boolean returnVal) { DeliveryPolicy policy = mock(DeliveryPolicy.class); when(policy.isAllowed()).thenReturn(returnVal); when(mockFactory.newConnectivityPolicy()).thenReturn(policy); return policy; } private DeliveryPolicy setupForceSubmissionTimePolicy(boolean returnVal) { DeliveryPolicy policy = mock(DeliveryPolicy.class); when(policy.isAllowed()).thenReturn(returnVal); when(mockFactory.newForceSubmissionTimePolicy()).thenReturn(policy); return policy; } private void verifyAndRunEventsExecutorService(int numExpectedRunnables) { ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mockEventExecutor, times(numExpectedRunnables)).execute(runnableCaptor.capture()); for (Runnable enqueueRunnable : runnableCaptor.getAllValues()) { enqueueRunnable.run(); } } private void verifyAndRunSubmissionExecutorService(int numExpectedRunnables) { ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mockSubmissionExecutor, times(numExpectedRunnables)).execute( runnableCaptor.capture()); for (Runnable enqueueRunnable : runnableCaptor.getAllValues()) { enqueueRunnable.run(); } } }