diff --git a/capture-sdk/sdk/build.gradle.kts b/capture-sdk/sdk/build.gradle.kts index 7fc531f536..8f89545fb1 100644 --- a/capture-sdk/sdk/build.gradle.kts +++ b/capture-sdk/sdk/build.gradle.kts @@ -190,7 +190,8 @@ dependencies { androidTestImplementation(libs.androidx.test.uiautomator) androidTestImplementation(libs.mockito.android) androidTestImplementation(libs.androidx.multidex) - + testImplementation(libs.mockito.kotlin2) + androidTestImplementation(libs.mockito.kotlin2) androidTestUtil(libs.androidx.test.orchestrator) } diff --git a/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java new file mode 100644 index 0000000000..a4dd6c01a4 --- /dev/null +++ b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/GiniCaptureHelperForInstrumentationTests.java @@ -0,0 +1,13 @@ +package net.gini.android.capture; + +import androidx.annotation.Nullable; + +/** + * Helper class to set the {@link GiniCapture} instance for instrumentation tests. + */ +public class GiniCaptureHelperForInstrumentationTests { + public static void setGiniCaptureInstance(@Nullable final GiniCapture giniCapture) { + GiniCapture.setInstance(giniCapture); + } + +} diff --git a/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt new file mode 100644 index 0000000000..3c250a48a0 --- /dev/null +++ b/capture-sdk/sdk/src/androidTest/java/net/gini/android/capture/ginicapture/GiniCaptureFragmentTest.kt @@ -0,0 +1,185 @@ +package net.gini.android.capture.ginicapture + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import androidx.fragment.app.testing.FragmentScenario +import androidx.lifecycle.Lifecycle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import jersey.repackaged.jsr166e.CompletableFuture +import net.gini.android.capture.DocumentImportEnabledFileTypes +import net.gini.android.capture.EntryPoint +import net.gini.android.capture.GiniCapture +import net.gini.android.capture.GiniCaptureFragment +import net.gini.android.capture.GiniCaptureHelperForInstrumentationTests +import net.gini.android.capture.di.CaptureSdkIsolatedKoinContext +import net.gini.android.capture.internal.document.ImageMultiPageDocumentMemoryStore +import net.gini.android.capture.internal.network.Configuration +import net.gini.android.capture.internal.network.ConfigurationNetworkResult +import net.gini.android.capture.internal.network.NetworkRequestsManager +import net.gini.android.capture.internal.provider.GiniBankConfigurationProvider +import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker +import net.gini.android.capture.tracking.useranalytics.UserAnalytics +import net.gini.android.capture.view.DefaultLoadingIndicatorAdapter +import net.gini.android.capture.view.DefaultNavigationBarTopAdapter +import net.gini.android.capture.view.InjectedViewAdapterInstance +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.dsl.module +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import java.util.UUID + +/** + * Integration test to verify the correct behavior of Analytics. + * Classes involved + * - [GiniCaptureFragment] + * - [GiniCapture] + * - [GiniCapture.Internal] + * - [NetworkRequestsManager] + * - [BufferedUserAnalyticsEventTracker] + * */ + +@RunWith(AndroidJUnit4::class) +class GiniCaptureFragmentTest { + private lateinit var networkRequestsManager: NetworkRequestsManager + private lateinit var giniCapture: GiniCapture + private lateinit var giniInternal: GiniCapture.Internal + private lateinit var memoryStore: ImageMultiPageDocumentMemoryStore + private val koinTestModule = module { + single { GiniBankConfigurationProvider() } + } + + /** + * We are using multiple dependencies in different classes, so many mocks are needed + * to fully test the functionality of AnalyticsTracker, by running the GiniCaptureFragment + * in Isolation. + * Mock was needed for [NetworkRequestsManager], [GiniCapture], [GiniCapture.Internal], + * [ImageMultiPageDocumentMemoryStore], [GiniBankConfigurationProvider] + * Also, in [GiniCaptureFragment], we are using koin to update + * the [GiniBankConfigurationProvider], for that we need to load and unload the module, + * other wise an exception will be thrown from koin for not loaded module. + * In the end, we need to set the mocked [GiniCapture] instance, and we have a helper class + * [GiniCaptureHelperForInstrumentationTests] for that. + * + * */ + + @Before + fun setUp() { + CaptureSdkIsolatedKoinContext.koin.loadModules(listOf(koinTestModule)) + + networkRequestsManager = mock() + giniCapture = mock() + giniInternal = mock() + memoryStore = mock() + + whenever(giniInternal.networkRequestsManager).thenReturn(networkRequestsManager) + whenever(giniCapture.internal()).thenReturn(giniInternal) + whenever(giniCapture.entryPoint).thenReturn(EntryPoint.BUTTON) + whenever(giniInternal.imageMultiPageDocumentMemoryStore).thenReturn(memoryStore) + whenever(giniInternal.navigationBarTopAdapterInstance).thenReturn( + InjectedViewAdapterInstance(DefaultNavigationBarTopAdapter()) + ) + whenever(giniCapture.documentImportEnabledFileTypes).thenReturn( + DocumentImportEnabledFileTypes.NONE + ) + whenever(giniCapture.internal().loadingIndicatorAdapterInstance).thenReturn( + InjectedViewAdapterInstance(DefaultLoadingIndicatorAdapter()) + ) + + GiniCaptureHelperForInstrumentationTests.setGiniCaptureInstance(giniCapture) + } + + + /** + * Unload the koin modules which were loaded in the [setUp]. + * */ + + @After + fun tearDown() = CaptureSdkIsolatedKoinContext.koin.unloadModules(listOf(koinTestModule)) + + + @Test + fun analyticsTracker_shouldBeEmpty_whenUserJourneyDisabled() { + + whenever(networkRequestsManager.getConfigurations(any())).thenReturn( + CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = false)) + ) + + launchGiniCaptureFragment().use { scenario -> + + scenario.moveToState(Lifecycle.State.STARTED) + + scenario.onFragment { _ -> + assertThat(getAnalyticsTracker().getTrackers()).isEmpty() + } + } + } + + + @Test + fun analyticsTracker_shouldNotBeEmpty_whenUserJourneyEnabled() { + + whenever(networkRequestsManager.getConfigurations(any())).thenReturn( + CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = true)) + ) + + launchGiniCaptureFragment().use { scenario -> + + scenario.moveToState(Lifecycle.State.STARTED) + + scenario.onFragment { _ -> + assertThat(getAnalyticsTracker().getTrackers()).isNotEmpty() + } + } + } + + + private fun getMockedConfiguration(userJourneyEnabled: Boolean): ConfigurationNetworkResult { + val testConfig = Configuration( + id = UUID.randomUUID(), + clientID = TEST_CLIENT_ID, + isUserJourneyAnalyticsEnabled = userJourneyEnabled, + isSkontoEnabled = false, + isReturnAssistantEnabled = false, + isTransactionDocsEnabled = false, + isQrCodeEducationEnabled = false, + isInstantPaymentEnabled = false, + isEInvoiceEnabled = false, + amplitudeApiKey = TEST_API_KEY + ) + + return ConfigurationNetworkResult(testConfig, UUID.randomUUID()) + } + + private fun getAnalyticsTracker(): BufferedUserAnalyticsEventTracker { + return UserAnalytics.getAnalyticsEventTracker() as BufferedUserAnalyticsEventTracker + } + + /** + * Helper method to launch the [GiniCaptureFragment] in a container, + * needed in all the tests. + * + * */ + + private fun launchGiniCaptureFragment(): FragmentScenario { + return FragmentScenario.launchInContainer( + fragmentClass = GiniCaptureFragment::class.java, + factory = object : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + return GiniCaptureFragment.createInstance().apply { + setListener(mock()) + } + } + } + ) + } + + companion object { + private const val TEST_CLIENT_ID = "test-client-id" + private const val TEST_API_KEY = "test-api-key" + } +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt index d680fcfa4b..2a46fea71d 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/BufferedUserAnalyticsEventTracker.kt @@ -1,6 +1,7 @@ package net.gini.android.capture.tracking.useranalytics import android.content.Context +import androidx.annotation.VisibleForTesting import net.gini.android.capture.internal.network.NetworkRequestsManager import net.gini.android.capture.internal.provider.UniqueIdProvider import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsEventProperty @@ -64,52 +65,53 @@ internal class BufferedUserAnalyticsEventTracker( } - override fun setEventSuperProperty(property: Set) { + override fun setEventSuperProperty(property: Set): Boolean { if (!mIsUserJourneyEnabled) - return + return false this.eventSuperProperties.add(property) - trySendEvents() + return trySendEvents() } - override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) { - setEventSuperProperty(setOf(property)) + override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty): Boolean { + return setEventSuperProperty(setOf(property)) } - override fun setUserProperty(userProperties: Set) { + override fun setUserProperty(userProperties: Set): Boolean { if (!mIsUserJourneyEnabled) - return + return false this.userProperties.add(userProperties) - trySendEvents() + return trySendEvents() } - override fun setUserProperty(userProperty: UserAnalyticsUserProperty) { - setUserProperty(setOf(userProperty)) + override fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean { + return setUserProperty(setOf(userProperty)) } override fun trackEvent( eventName: UserAnalyticsEvent, properties: Set - ) { + ): Boolean { if (!mIsUserJourneyEnabled) - return + return false events.add(Pair(eventName, properties)) - trySendEvents() + return trySendEvents() } - override fun trackEvent(eventName: UserAnalyticsEvent) { - trackEvent(eventName, emptySet()) + override fun trackEvent(eventName: UserAnalyticsEvent): Boolean { + return trackEvent(eventName, emptySet()) } - override fun flushEvents() { - amplitude?.flushEvents() + override fun flushEvents(): Boolean { + return amplitude?.let { + amplitude?.flushEvents() + } ?: false } - private fun trySendEvents() { - if (!mIsUserJourneyEnabled) - return - if (eventTrackers.isEmpty()) { - LOG.debug("No trackers found. Skipping sending events") - return + private fun trySendEvents(): Boolean { + if (!mIsUserJourneyEnabled || eventTrackers.isEmpty()) { + if (eventTrackers.isEmpty()) + LOG.debug("No trackers found. Skipping sending events") + return false } LOG.debug("${eventTrackers.size} Tracker(s) found. Sending events...") @@ -135,10 +137,14 @@ internal class BufferedUserAnalyticsEventTracker( LOG.debug("Events sent") + return true } private fun everyTracker(block: (UserAnalyticsEventTracker) -> Unit) { eventTrackers.forEach(block) } + @VisibleForTesting + internal fun getTrackers(): Set = eventTrackers.toSet() + } \ No newline at end of file diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt index 4b86086224..73f475b310 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt @@ -7,19 +7,22 @@ import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsU interface UserAnalyticsEventTracker { - fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) + fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty): Boolean - fun setEventSuperProperty(property: Set) + fun setEventSuperProperty(property: Set): Boolean - fun setUserProperty(userProperty: UserAnalyticsUserProperty) + fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean - fun setUserProperty(userProperties: Set) + fun setUserProperty(userProperties: Set): Boolean - fun trackEvent(eventName: UserAnalyticsEvent) + fun trackEvent(eventName: UserAnalyticsEvent): Boolean - fun trackEvent(eventName: UserAnalyticsEvent, properties: Set) + fun trackEvent( + eventName: UserAnalyticsEvent, + properties: Set + ): Boolean - fun flushEvents() + fun flushEvents(): Boolean } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt index 017b4e0e93..6f257fb449 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/tracker/AmplitudeUserAnalyticsEventTracker.kt @@ -45,24 +45,26 @@ internal class AmplitudeUserAnalyticsEventTracker( context ) - override fun setUserProperty(userProperties: Set) { + override fun setUserProperty(userProperties: Set) : Boolean { this.userProperties = userProperties.associate { it.getPair() } + return true } - override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) { - setEventSuperProperty(setOf(property)) + override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) : Boolean{ + return setEventSuperProperty(setOf(property)) } - override fun setEventSuperProperty(property: Set) { + override fun setEventSuperProperty(property: Set) : Boolean{ superProperties.addAll(property) + return true } - override fun setUserProperty(userProperty: UserAnalyticsUserProperty) { - setUserProperty(setOf(userProperty)) + override fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean { + return setUserProperty(setOf(userProperty)) } - override fun trackEvent(eventName: UserAnalyticsEvent) { - trackEvent(eventName, emptySet()) + override fun trackEvent(eventName: UserAnalyticsEvent) : Boolean{ + return trackEvent(eventName, emptySet()) } private val events: MutableList = mutableListOf() @@ -70,7 +72,7 @@ internal class AmplitudeUserAnalyticsEventTracker( override fun trackEvent( eventName: UserAnalyticsEvent, properties: Set - ) { + ) : Boolean{ val superPropertiesMap = superProperties.associate { it.getPair() } val propertiesMap = properties.associate { it.getPair() } @@ -102,6 +104,7 @@ internal class AmplitudeUserAnalyticsEventTracker( LOG.debug("\nEvent: ${eventName.eventName}\n" + finalProperties.toList().joinToString("\n") { " ${it.first}=${it.second}" }) + return true } fun startRepeatingJob(): Job { @@ -113,10 +116,11 @@ internal class AmplitudeUserAnalyticsEventTracker( } } - override fun flushEvents() { + override fun flushEvents() : Boolean{ CoroutineScope(Dispatchers.IO).launch { sendEventsToAmplitudeApi() } + return true } private fun sendEventsToAmplitudeApi() { diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt index 0fd65844c1..0dd5783f08 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/camera/CameraFragmentImplTest.kt @@ -6,13 +6,8 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import com.nhaarman.mockitokotlin2.* -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk import jersey.repackaged.jsr166e.CompletableFuture import net.gini.android.capture.GiniCapture import net.gini.android.capture.internal.camera.api.CameraInterface @@ -100,8 +95,8 @@ class CameraFragmentImplTest { on { visibility } doReturn View.INVISIBLE } val analyticsTrackerMock = mock { - on { trackEvent(any()) }.then {} - on { trackEvent(any(), any()) }.then {} + on { trackEvent(any()) }.thenReturn(true) + on { trackEvent(any(), any()) }.thenReturn(true) } fragmentImpl.mLayoutNoPermission = noPermissionLayoutMock diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt index 7a4fdbe6ce..df978634fa 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/onboarding/OnboardingScreenPresenterTest.kt @@ -4,12 +4,9 @@ import android.app.Activity import com.google.common.collect.Lists import com.google.common.truth.Correspondence import com.google.common.truth.Truth -import io.mockk.Runs import io.mockk.every -import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify import junitparams.JUnitParamsRunner @@ -53,8 +50,8 @@ class OnboardingScreenPresenterTest { @Before fun setUp() { mUserAnalyticsEventTracker = mockk().apply { - every { trackEvent(any()) } just Runs - every { trackEvent(any(), any()) } just Runs + every { trackEvent(any()) } returns true + every { trackEvent(any(), any()) } returns true } mActivity = mockk() diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt new file mode 100644 index 0000000000..9ba40844f5 --- /dev/null +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/tracking/BufferedUserAnalyticsEventTrackerTest.kt @@ -0,0 +1,115 @@ +package net.gini.android.capture.tracking + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.mockk +import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker +import net.gini.android.capture.tracking.useranalytics.UserAnalyticsEvent +import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsUserProperty +import net.gini.android.capture.tracking.useranalytics.tracker.AmplitudeUserAnalyticsEventTracker +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse + +/** + * Tests the behavior of [BufferedUserAnalyticsEventTracker]. + * + * If userJourney is disabled, no tracker is added and events/properties return false. + * If enabled, trackers are added and methods return true. + * + * Each test verifies this behavior by checking method results instead of internal queues. + */ + +@RunWith(AndroidJUnit4::class) +class BufferedUserAnalyticsEventTrackerTest { + + private lateinit var tracker: BufferedUserAnalyticsEventTracker + private val mockContext: Context = mockk(relaxed = true) + + // Random testSessionId for testing + private val testSessionId = "test-session" + + // Random api key for testing + private val testApiKey = "test-api-key" + + // This is an arbitrary version for testing + private val testCaptureVersion = "3.10.1" + + @Before + fun setup() { + tracker = BufferedUserAnalyticsEventTracker(mockContext, testSessionId) + } + + @Test + fun `trackEvent returns false when userJourney is disabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = false) + + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) + + assertFalse(result) + } + + + @Test + fun `trackEvent returns true when userJourney is enabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = true) + + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED) + + assertTrue(result) + } + + + @Test + fun `setEventSuperProperty returns false when userJourney is disabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = false) + + val result = tracker.setEventSuperProperty(emptySet()) + + assertFalse(result) + } + + @Test + fun `setEventSuperProperty returns true when userJourney is enabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = true) + + val result = tracker.setEventSuperProperty(emptySet()) + + assertTrue(result) + } + + @Test + fun `trackEvent with properties returns false when userJourney is disabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = false) + + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) + + assertFalse(result) + } + + @Test + fun `trackEvent with properties returns true when userJourney is enabled`() { + setupTrackerWithUserJourney(isUserJourneyEnabled = true) + + val result = tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet()) + + assertTrue(result) + } + + private fun setupTrackerWithUserJourney(isUserJourneyEnabled: Boolean) { + + tracker.setPlatformTokens( + AmplitudeUserAnalyticsEventTracker.AmplitudeAnalyticsApiKey(testApiKey), + networkRequestsManager = mockk(relaxed = true), + isUserJourneyEnabled = isUserJourneyEnabled + ) + + tracker.setUserProperty( + setOf( + UserAnalyticsUserProperty.CaptureSdkVersionName(testCaptureVersion) + ) + ) + } +}