From 619c17b5f5077d368e52a3ac44642240cb409cb6 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 30 Jan 2023 16:03:58 -0600 Subject: [PATCH] Convert AnalyticsClient to Kotlin. --- BraintreeCore/build.gradle | 3 +- .../api/AnalyticsClientUnitTest.java | 420 ----------------- .../api/AnalyticsClientUnitTest.kt | 433 ++++++++++++++++++ SharedUtils/build.gradle | 2 + .../api/HttpResponseCallback.java | 9 - .../api/HttpResponseCallback.kt | 14 + 6 files changed, 451 insertions(+), 430 deletions(-) delete mode 100644 BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.java create mode 100644 BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt delete mode 100644 SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.java create mode 100644 SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.kt diff --git a/BraintreeCore/build.gradle b/BraintreeCore/build.gradle index 7320afad11..41206ac62a 100644 --- a/BraintreeCore/build.gradle +++ b/BraintreeCore/build.gradle @@ -7,7 +7,7 @@ plugins { id 'kotlin-kapt' } -def DEVELOPMENT_URL = System.properties['DEVELOPMENT_URL'] ?: '"http://10.0.2.2:3000/"'; +def DEVELOPMENT_URL = System.properties['DEVELOPMENT_URL'] ?: '"http://10.0.2.2:3000/"' android { compileSdkVersion rootProject.compileSdkVersion @@ -84,6 +84,7 @@ dependencies { testImplementation deps.powermockMockito testImplementation deps.powermockClassloading testImplementation deps.jsonAssert + testImplementation deps.mockk testImplementation project(':PayPal') testImplementation project(':TestUtils') testImplementation project(':UnionPay') diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.java b/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.java deleted file mode 100644 index 75860c64f7..0000000000 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.java +++ /dev/null @@ -1,420 +0,0 @@ -package com.braintreepayments.api; - -import static com.braintreepayments.api.AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION; -import static com.braintreepayments.api.AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION; -import static com.braintreepayments.api.AnalyticsClient.WORK_INPUT_KEY_EVENT_NAME; -import static com.braintreepayments.api.AnalyticsClient.WORK_INPUT_KEY_INTEGRATION; -import static com.braintreepayments.api.AnalyticsClient.WORK_INPUT_KEY_SESSION_ID; -import static com.braintreepayments.api.AnalyticsClient.WORK_INPUT_KEY_TIMESTAMP; -import static junit.framework.Assert.assertEquals; -import static junit.framework.TestCase.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import android.content.Context; - -import androidx.test.core.app.ApplicationProvider; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.ListenableWorker; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import androidx.work.impl.model.WorkSpec; - -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.robolectric.RobolectricTestRunner; -import org.skyscreamer.jsonassert.JSONAssert; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.List; - -@RunWith(RobolectricTestRunner.class) -public class AnalyticsClientUnitTest { - - private Context context; - private Authorization authorization; - - private BraintreeHttpClient httpClient; - private DeviceInspector deviceInspector; - - private String eventName; - private long timestamp; - private String sessionId; - private String integration; - - private WorkManager workManager; - private AnalyticsDatabase analyticsDatabase; - private AnalyticsEventDao analyticsEventDao; - - @Before - public void beforeEach() throws InvalidArgumentException, GeneralSecurityException, IOException { - timestamp = 123; - eventName = "sample-event-name"; - sessionId = "sample-session-id"; - integration = "sample-integration"; - - authorization = Authorization.fromString(Fixtures.TOKENIZATION_KEY); - context = ApplicationProvider.getApplicationContext(); - - httpClient = mock(BraintreeHttpClient.class); - deviceInspector = mock(DeviceInspector.class); - - analyticsDatabase = mock(AnalyticsDatabase.class); - analyticsEventDao = mock(AnalyticsEventDao.class); - when(analyticsDatabase.analyticsEventDao()).thenReturn(analyticsEventDao); - - workManager = mock(WorkManager.class); - } - - @Test - public void sendEvent_enqueuesAnalyticsWriteToDbWorker() throws JSONException { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.sendEvent(configuration, eventName, sessionId, integration, 123, authorization); - - ArgumentCaptor captor = ArgumentCaptor.forClass(OneTimeWorkRequest.class); - verify(workManager) - .enqueueUniqueWork(eq("writeAnalyticsToDb"), eq(ExistingWorkPolicy.APPEND_OR_REPLACE), captor.capture()); - - OneTimeWorkRequest workRequest = captor.getValue(); - WorkSpec workSpec = workRequest.getWorkSpec(); - assertEquals(AnalyticsWriteToDbWorker.class.getName(), workSpec.workerClassName); - - assertEquals(authorization.toString(), workSpec.input.getString("authorization")); - assertEquals("android.sample-event-name", workSpec.input.getString("eventName")); - assertEquals(123, workSpec.input.getLong("timestamp", 0)); - } - - @Test - public void sendEvent_enqueuesAnalyticsUploadWorker() throws JSONException { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.sendEvent(configuration, eventName, sessionId, integration, 123, authorization); - - ArgumentCaptor captor = ArgumentCaptor.forClass(OneTimeWorkRequest.class); - verify(workManager) - .enqueueUniqueWork(eq("uploadAnalytics"), eq(ExistingWorkPolicy.KEEP), captor.capture()); - - OneTimeWorkRequest workRequest = captor.getValue(); - WorkSpec workSpec = workRequest.getWorkSpec(); - assertEquals(30000, workSpec.initialDelay); - assertEquals(AnalyticsUploadWorker.class.getName(), workSpec.workerClassName); - - assertEquals(configuration.toJson(), workSpec.input.getString("configuration")); - assertEquals(authorization.toString(), workSpec.input.getString("authorization")); - assertEquals("sample-session-id", workSpec.input.getString("sessionId")); - assertEquals("sample-integration", workSpec.input.getString("integration")); - } - - @Test - public void writeAnalytics_whenEventNameAndTimestampArePresent_returnsSuccess() { - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_EVENT_NAME, eventName) - .putLong(WORK_INPUT_KEY_TIMESTAMP, timestamp) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - ListenableWorker.Result result = sut.writeAnalytics(inputData); - assertTrue(result instanceof ListenableWorker.Result.Success); - } - - @Test - public void writeAnalytics_whenEventNameIsMissing_returnsFailure() { - Data inputData = new Data.Builder() - .putLong(WORK_INPUT_KEY_TIMESTAMP, timestamp) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - ListenableWorker.Result result = sut.writeAnalytics(inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - } - - @Test - public void writeAnalytics_whenTimestampIsMissing_returnsFailure() { - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_EVENT_NAME, eventName) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - ListenableWorker.Result result = sut.writeAnalytics(inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - } - - @Test - public void writeAnalytics_addsEventToAnalyticsDatabaseAndReturnsSuccess() { - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_EVENT_NAME, eventName) - .putLong(WORK_INPUT_KEY_TIMESTAMP, timestamp) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.writeAnalytics(inputData); - - ArgumentCaptor captor = ArgumentCaptor.forClass(AnalyticsEvent.class); - verify(analyticsEventDao).insertEvent(captor.capture()); - - AnalyticsEvent event = captor.getValue(); - assertEquals("sample-event-name", event.getName()); - assertEquals(123, event.getTimestamp()); - } - - @Test - public void uploadAnalytics_whenNoEventsExist_doesNothing() throws Exception { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.uploadAnalytics(context, inputData); - - verifyZeroInteractions(httpClient); - } - - @Test - public void uploadAnalytics_whenEventsExist_sendsAllEvents() throws Exception { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - DeviceMetadata metadata = createSampleDeviceMetadata(); - when(deviceInspector.getDeviceMetadata(context, sessionId, integration)).thenReturn(metadata); - - List events = new ArrayList<>(); - events.add(new AnalyticsEvent("event0", 123)); - events.add(new AnalyticsEvent("event1", 456)); - - when(analyticsEventDao.getAllEvents()).thenReturn(events); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.uploadAnalytics(context, inputData); - - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(httpClient).post(anyString(), captor.capture(), any(Configuration.class), any(Authorization.class)); - - JSONObject analyticsJson = new JSONObject(captor.getValue()); - - JSONObject meta = analyticsJson.getJSONObject("_meta"); - JSONAssert.assertEquals(metadata.toJSON(), meta, true); - - JSONArray array = analyticsJson.getJSONArray("analytics"); - assertEquals(2, array.length()); - - JSONObject eventOne = array.getJSONObject(0); - assertEquals("event0", eventOne.getString("kind")); - assertEquals(123, Long.parseLong(eventOne.getString("timestamp"))); - - JSONObject eventTwo = array.getJSONObject(1); - assertEquals("event1", eventTwo.getString("kind")); - assertEquals(456, Long.parseLong(eventTwo.getString("timestamp"))); - } - - @Test - public void uploadAnalytics_whenConfigurationIsNull_doesNothing() { - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - - ListenableWorker.Result result = sut.uploadAnalytics(context, inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - verifyZeroInteractions(httpClient); - } - - @Test - public void uploadAnalytics_whenAuthorizationIsNull_doesNothing() throws JSONException { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - - ListenableWorker.Result result = sut.uploadAnalytics(context, inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - verifyZeroInteractions(httpClient); - } - - @Test - public void uploadAnalytics_whenSessionIdIsNull_doesNothing() throws JSONException { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - - ListenableWorker.Result result = sut.uploadAnalytics(context, inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - verifyZeroInteractions(httpClient); - } - - @Test - public void uploadAnalytics_whenIntegrationIsNull_doesNothing() throws JSONException { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .build(); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - - ListenableWorker.Result result = sut.uploadAnalytics(context, inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - verifyZeroInteractions(httpClient); - } - - @Test - public void uploadAnalytics_deletesDatabaseEventsOnSuccessResponse() throws Exception { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - DeviceMetadata metadata = createSampleDeviceMetadata(); - when(deviceInspector.getDeviceMetadata(context, sessionId, integration)).thenReturn(metadata); - - List events = new ArrayList<>(); - events.add(new AnalyticsEvent("event0", 123)); - events.add(new AnalyticsEvent("event1", 456)); - - when(analyticsEventDao.getAllEvents()).thenReturn(events); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.uploadAnalytics(context, inputData); - - verify(analyticsEventDao).deleteEvents(events); - } - - @Test - public void uploadAnalytics_whenAnalyticsSendFails_returnsError() throws Exception { - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - Data inputData = new Data.Builder() - .putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) - .putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) - .putString(WORK_INPUT_KEY_SESSION_ID, sessionId) - .putString(WORK_INPUT_KEY_INTEGRATION, integration) - .build(); - - DeviceMetadata metadata = createSampleDeviceMetadata(); - when(deviceInspector.getDeviceMetadata(context, sessionId, integration)).thenReturn(metadata); - - List events = new ArrayList<>(); - events.add(new AnalyticsEvent("event0", 123)); - events.add(new AnalyticsEvent("event1", 456)); - - when(analyticsEventDao.getAllEvents()).thenReturn(events); - - Exception httpError = new Exception("error"); - when(httpClient.post(anyString(), anyString(), any(Configuration.class), any(Authorization.class))).thenThrow(httpError); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - ListenableWorker.Result result = sut.uploadAnalytics(context, inputData); - assertTrue(result instanceof ListenableWorker.Result.Failure); - } - - @Test - public void reportCrash_whenLastKnownAnalyticsUrlExists_sendsCrashAnalyticsEvent() throws Exception { - DeviceMetadata metadata = createSampleDeviceMetadata(); - when(deviceInspector.getDeviceMetadata(context, sessionId, integration)).thenReturn(metadata); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - - sut.sendEvent(configuration, eventName, sessionId, integration, authorization); - sut.reportCrash(context, sessionId, integration, 123, authorization); - - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(httpClient).post(eq("analytics_url"), captor.capture(), (Configuration) isNull(), same(authorization), any(HttpNoResponse.class)); - - JSONObject analyticsJson = new JSONObject(captor.getValue()); - - JSONObject meta = analyticsJson.getJSONObject("_meta"); - JSONAssert.assertEquals(metadata.toJSON(), meta, true); - - JSONArray array = analyticsJson.getJSONArray("analytics"); - assertEquals(1, array.length()); - - JSONObject eventOne = array.getJSONObject(0); - assertEquals("android.crash", eventOne.getString("kind")); - assertEquals(123, Long.parseLong(eventOne.getString("timestamp"))); - } - - @Test - public void reportCrash_whenLastKnownAnalyticsUrlMissing_doesNothing() throws JSONException { - DeviceMetadata metadata = createSampleDeviceMetadata(); - when(deviceInspector.getDeviceMetadata(context, sessionId, integration)).thenReturn(metadata); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - sut.reportCrash(context, sessionId, integration, 123, authorization); - - verifyZeroInteractions(httpClient); - } - - @Test - public void reportCrash_whenAuthorizationIsNull_doesNothing() throws JSONException { - DeviceMetadata metadata = createSampleDeviceMetadata(); - when(deviceInspector.getDeviceMetadata(context, sessionId, integration)).thenReturn(metadata); - - AnalyticsClient sut = new AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector); - Configuration configuration = Configuration.fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS); - - sut.sendEvent(configuration, eventName, sessionId, integration, authorization); - sut.reportCrash(context, sessionId, integration, 123, null); - verifyZeroInteractions(httpClient); - } - - private static DeviceMetadata createSampleDeviceMetadata() { - return new DeviceMetadata.Builder() - .integration("sample-integration") - .sessionId("sample-session-id") - .platform("platform") - .sdkVersion("sdk-version") - .deviceManufacturer("device-manufacturer") - .deviceModel("device-model") - .platformVersion("platform-version") - .merchantAppName("merchant-app-name") - .devicePersistentUUID("persistent-uuid") - .merchantAppId("merchant-app-id") - .userOrientation("user-orientation") - .isPayPalInstalled(true) - .isVenmoInstalled(true) - .isSimulator(false) - .build(); - } -} diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt new file mode 100644 index 0000000000..ded8251eef --- /dev/null +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt @@ -0,0 +1,433 @@ +package com.braintreepayments.api + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.work.* +import com.braintreepayments.api.Authorization.Companion.fromString +import com.braintreepayments.api.Configuration.Companion.fromJson +import io.mockk.* +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.skyscreamer.jsonassert.JSONAssert +import java.io.IOException +import java.security.GeneralSecurityException + +@RunWith(RobolectricTestRunner::class) +class AnalyticsClientUnitTest { + + private lateinit var context: Context + private lateinit var authorization: Authorization + private lateinit var httpClient: BraintreeHttpClient + private lateinit var deviceInspector: DeviceInspector + private lateinit var eventName: String + private lateinit var sessionId: String + private lateinit var integration: String + private lateinit var workManager: WorkManager + private lateinit var analyticsDatabase: AnalyticsDatabase + private lateinit var analyticsEventDao: AnalyticsEventDao + + private var timestamp: Long = 0 + + @Before + @Throws(InvalidArgumentException::class, GeneralSecurityException::class, IOException::class) + fun beforeEach() { + timestamp = 123 + eventName = "sample-event-name" + sessionId = "sample-session-id" + integration = "sample-integration" + authorization = fromString(Fixtures.TOKENIZATION_KEY) + context = ApplicationProvider.getApplicationContext() + httpClient = mockk(relaxed = true) + deviceInspector = mockk(relaxed = true) + analyticsDatabase = mockk(relaxed = true) + analyticsEventDao = mockk(relaxed = true) + workManager = mockk(relaxed = true) + + every { analyticsDatabase.analyticsEventDao() } returns analyticsEventDao + } + + @Test + @Throws(JSONException::class) + fun sendEvent_enqueuesAnalyticsWriteToDbWorker() { + val workRequestSlot = slot() + every { + workManager.enqueueUniqueWork( + "writeAnalyticsToDb", + ExistingWorkPolicy.APPEND_OR_REPLACE, + capture(workRequestSlot) + ) + } returns mockk() + + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.sendEvent(configuration, eventName, sessionId, integration, 123, authorization) + + val workSpec = workRequestSlot.captured.workSpec + assertEquals(AnalyticsWriteToDbWorker::class.java.name, workSpec.workerClassName) + assertEquals(authorization.toString(), workSpec.input.getString("authorization")) + assertEquals("android.sample-event-name", workSpec.input.getString("eventName")) + assertEquals(123, workSpec.input.getLong("timestamp", 0)) + } + + @Test + @Throws(JSONException::class) + fun sendEvent_enqueuesAnalyticsUploadWorker() { + val workRequestSlot = slot() + every { + workManager.enqueueUniqueWork( + "uploadAnalytics", + ExistingWorkPolicy.KEEP, + capture(workRequestSlot) + ) + } returns mockk() + + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.sendEvent(configuration, eventName, sessionId, integration, 123, authorization) + + val workSpec = workRequestSlot.captured.workSpec + assertEquals(30000, workSpec.initialDelay) + assertEquals(AnalyticsUploadWorker::class.java.name, workSpec.workerClassName) + assertEquals(configuration.toJson(), workSpec.input.getString("configuration")) + assertEquals(authorization.toString(), workSpec.input.getString("authorization")) + assertEquals("sample-session-id", workSpec.input.getString("sessionId")) + assertEquals("sample-integration", workSpec.input.getString("integration")) + } + + @Test + fun writeAnalytics_whenEventNameAndTimestampArePresent_returnsSuccess() { + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_EVENT_NAME, eventName) + .putLong(AnalyticsClient.WORK_INPUT_KEY_TIMESTAMP, timestamp) + .build() + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.writeAnalytics(inputData) + assertTrue(result is ListenableWorker.Result.Success) + } + + @Test + fun writeAnalytics_whenEventNameIsMissing_returnsFailure() { + val inputData = Data.Builder() + .putLong(AnalyticsClient.WORK_INPUT_KEY_TIMESTAMP, timestamp) + .build() + val sut = + AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.writeAnalytics(inputData) + assertTrue(result is ListenableWorker.Result.Failure) + } + + @Test + fun writeAnalytics_whenTimestampIsMissing_returnsFailure() { + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_EVENT_NAME, eventName) + .build() + val sut = + AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.writeAnalytics(inputData) + assertTrue(result is ListenableWorker.Result.Failure) + } + + @Test + fun writeAnalytics_addsEventToAnalyticsDatabaseAndReturnsSuccess() { + val analyticsEventSlot = slot() + every { analyticsEventDao.insertEvent(capture(analyticsEventSlot)) } returns Unit + + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_EVENT_NAME, eventName) + .putLong(AnalyticsClient.WORK_INPUT_KEY_TIMESTAMP, timestamp) + .build() + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.writeAnalytics(inputData) + + val event = analyticsEventSlot.captured + assertEquals("sample-event-name", event.name) + assertEquals(123, event.timestamp) + } + + @Test + @Throws(Exception::class) + fun uploadAnalytics_whenNoEventsExist_doesNothing() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.uploadAnalytics(context, inputData) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + @Test + @Throws(Exception::class) + fun uploadAnalytics_whenEventsExist_sendsAllEvents() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + val metadata = createSampleDeviceMetadata() + + every { + deviceInspector.getDeviceMetadata(context, sessionId, integration) + } returns metadata + + val events: MutableList = ArrayList() + events.add(AnalyticsEvent("event0", 123)) + events.add(AnalyticsEvent("event1", 456)) + every { analyticsEventDao.getAllEvents() } returns events + + val analyticsJSONSlot = slot() + every { httpClient.post(any(), capture(analyticsJSONSlot), any(), any()) } + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.uploadAnalytics(context, inputData) + + val analyticsJson = JSONObject(analyticsJSONSlot.captured) + val meta = analyticsJson.getJSONObject("_meta") + JSONAssert.assertEquals(metadata.toJSON(), meta, true) + + val array = analyticsJson.getJSONArray("analytics") + assertEquals(2, array.length()) + + val eventOne = array.getJSONObject(0) + assertEquals("event0", eventOne.getString("kind")) + assertEquals(123, eventOne.getString("timestamp").toLong()) + + val eventTwo = array.getJSONObject(1) + assertEquals("event1", eventTwo.getString("kind")) + assertEquals(456, eventTwo.getString("timestamp").toLong()) + } + + @Test + fun uploadAnalytics_whenConfigurationIsNull_doesNothing() { + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.uploadAnalytics(context, inputData) + assertTrue(result is ListenableWorker.Result.Failure) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + @Test + @Throws(JSONException::class) + fun uploadAnalytics_whenAuthorizationIsNull_doesNothing() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.uploadAnalytics(context, inputData) + assertTrue(result is ListenableWorker.Result.Failure) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + @Test + @Throws(JSONException::class) + fun uploadAnalytics_whenSessionIdIsNull_doesNothing() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.uploadAnalytics(context, inputData) + assertTrue(result is ListenableWorker.Result.Failure) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + @Test + @Throws(JSONException::class) + fun uploadAnalytics_whenIntegrationIsNull_doesNothing() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .build() + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.uploadAnalytics(context, inputData) + assertTrue(result is ListenableWorker.Result.Failure) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + @Test + @Throws(Exception::class) + fun uploadAnalytics_deletesDatabaseEventsOnSuccessResponse() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + + val metadata = createSampleDeviceMetadata() + every { + deviceInspector.getDeviceMetadata( + context, + sessionId, + integration + ) + } returns metadata + + val events: MutableList = ArrayList() + events.add(AnalyticsEvent("event0", 123)) + events.add(AnalyticsEvent("event1", 456)) + every { analyticsEventDao.getAllEvents() } returns events + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.uploadAnalytics(context, inputData) + + verify { analyticsEventDao.deleteEvents(events) } + } + + @Test + @Throws(Exception::class) + fun uploadAnalytics_whenAnalyticsSendFails_returnsError() { + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + val inputData = Data.Builder() + .putString(AnalyticsClient.WORK_INPUT_KEY_AUTHORIZATION, authorization.toString()) + .putString(AnalyticsClient.WORK_INPUT_KEY_CONFIGURATION, configuration.toJson()) + .putString(AnalyticsClient.WORK_INPUT_KEY_SESSION_ID, sessionId) + .putString(AnalyticsClient.WORK_INPUT_KEY_INTEGRATION, integration) + .build() + + val metadata = createSampleDeviceMetadata() + every { + deviceInspector.getDeviceMetadata(context, sessionId, integration) + } returns metadata + + val events: MutableList = ArrayList() + events.add(AnalyticsEvent("event0", 123)) + events.add(AnalyticsEvent("event1", 456)) + every { analyticsEventDao.getAllEvents() } returns events + + val httpError = Exception("error") + every { httpClient.post(any(), any(), any(), any()) } throws httpError + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val result = sut.uploadAnalytics(context, inputData) + assertTrue(result is ListenableWorker.Result.Failure) + } + + @Test + @Throws(Exception::class) + fun reportCrash_whenLastKnownAnalyticsUrlExists_sendsCrashAnalyticsEvent() { + val metadata = createSampleDeviceMetadata() + every { + deviceInspector.getDeviceMetadata(context, sessionId, integration) + } returns metadata + + val analyticsJSONSlot = slot() + every { + httpClient.post( + "analytics_url", + capture(analyticsJSONSlot), + isNull(), + authorization, + any() + ) + } returns Unit + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + sut.sendEvent(configuration, eventName, sessionId, integration, authorization) + + sut.reportCrash(context, sessionId, integration, 123, authorization) + + val analyticsJson = JSONObject(analyticsJSONSlot.captured) + val meta = analyticsJson.getJSONObject("_meta") + JSONAssert.assertEquals(metadata.toJSON(), meta, true) + + val array = analyticsJson.getJSONArray("analytics") + assertEquals(1, array.length()) + + val eventOne = array.getJSONObject(0) + assertEquals("android.crash", eventOne.getString("kind")) + assertEquals(123, eventOne.getString("timestamp").toLong()) + } + + @Test + @Throws(JSONException::class) + fun reportCrash_whenLastKnownAnalyticsUrlMissing_doesNothing() { + val metadata = createSampleDeviceMetadata() + every { + deviceInspector.getDeviceMetadata(context, sessionId, integration) + } returns metadata + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + sut.reportCrash(context, sessionId, integration, 123, authorization) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + @Test + @Throws(JSONException::class) + fun reportCrash_whenAuthorizationIsNull_doesNothing() { + val metadata = createSampleDeviceMetadata() + every { + deviceInspector.getDeviceMetadata(context, sessionId, integration) + } returns metadata + + val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) + val configuration = fromJson(Fixtures.CONFIGURATION_WITH_ANALYTICS) + sut.sendEvent(configuration, eventName, sessionId, integration, authorization) + + sut.reportCrash(context, sessionId, integration, 123, null) + + // or confirmVerified(httpClient) + verify { httpClient wasNot Called } + } + + companion object { + private fun createSampleDeviceMetadata(): DeviceMetadata { + return DeviceMetadata.Builder() + .integration("sample-integration") + .sessionId("sample-session-id") + .platform("platform") + .sdkVersion("sdk-version") + .deviceManufacturer("device-manufacturer") + .deviceModel("device-model") + .platformVersion("platform-version") + .merchantAppName("merchant-app-name") + .devicePersistentUUID("persistent-uuid") + .merchantAppId("merchant-app-id") + .userOrientation("user-orientation") + .isPayPalInstalled(true) + .isVenmoInstalled(true) + .isSimulator(false) + .build() + } + } +} \ No newline at end of file diff --git a/SharedUtils/build.gradle b/SharedUtils/build.gradle index 99cb3932b0..37a18e0657 100644 --- a/SharedUtils/build.gradle +++ b/SharedUtils/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.library' + id 'kotlin-android' id 'de.marcphilipp.nexus-publish' id 'signing' } @@ -37,6 +38,7 @@ android { dependencies { implementation deps.annotation implementation deps.securityCrypto + implementation deps.kotlinStdLib testImplementation deps.junit testImplementation deps.androidxTestCore diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.java b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.java deleted file mode 100644 index c8a44a8ace..0000000000 --- a/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.braintreepayments.api; - -import androidx.annotation.MainThread; - -interface HttpResponseCallback { - - @MainThread - void onResult(String responseBody, Exception httpError); -} diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.kt b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.kt new file mode 100644 index 0000000000..b2cae6190b --- /dev/null +++ b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseCallback.kt @@ -0,0 +1,14 @@ +package com.braintreepayments.api + +import androidx.annotation.MainThread +import androidx.annotation.RestrictTo + +/** + * @suppress + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface HttpResponseCallback { + + @MainThread + fun onResult(responseBody: String?, httpError: Exception?) +}