diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index fbd0abc7d..ece881dd8 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -29,7 +29,7 @@ lint: - actionlint@1.7.7 - checkov@3.2.470 - git-diff-check - - ktlint@0.48.2 + - ktlint@1.7.1 - markdownlint@0.45.0 - prettier@3.6.2 - shellcheck@0.11.0 diff --git a/android-core/proguard.pro b/android-core/proguard.pro index d522ffa4a..400222184 100644 --- a/android-core/proguard.pro +++ b/android-core/proguard.pro @@ -145,6 +145,7 @@ -keep class com.mparticle.internal.Logger$* { *; } -keep class com.mparticle.internal.KitsLoadedCallback { *; } -keep class com.mparticle.internal.listeners.InternalListenerManager { *; } +-keep class com.mparticle.internal.RoktKitApi { *; } -keep class com.mparticle.identity.IdentityApi { *; } -keep class com.mparticle.identity.IdentityApiRequest { *; } diff --git a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java index f6d7bc933..1065d4102 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java +++ b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java @@ -19,15 +19,11 @@ import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; import com.mparticle.WrapperSdkVersion; import com.mparticle.consent.ConsentState; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; import com.mparticle.internal.listeners.InternalListenerManager; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -43,8 +39,6 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentLinkedQueue; -import kotlinx.coroutines.flow.Flow; - public class KitFrameworkWrapper implements KitManager { private final Context mContext; final CoreCallbacks mCoreCallbacks; @@ -669,29 +663,12 @@ public void reset() { } @Override - public void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config) { - if (mKitManager != null) { - mKitManager.execute(viewName, - attributes, - mpRoktEventCallback, - placeHolders, - fontTypefaces, - config); - } - } - - @Override - public Flow events(@NonNull String identifier) { + @Nullable + public RoktKitApi getRoktKitApi() { if (mKitManager != null) { - return mKitManager.events(identifier); - } else { - return flowOf(); + return mKitManager.getRoktKitApi(); } + return null; } @Override @@ -701,43 +678,6 @@ public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { } } - @Override - public void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status) { - if (mKitManager != null) { - mKitManager.purchaseFinalized(placementId, catalogItemId, status); - } - } - - @Override - public void close() { - if (mKitManager != null) { - mKitManager.close(); - } - } - - @Override - public void prepareAttributesAsync(@NonNull Map attributes) { - if (mKitManager != null) { - mKitManager.prepareAttributesAsync(attributes); - } - } - - @Override - public void setSessionId(@NonNull String sessionId) { - if (mKitManager != null) { - mKitManager.setSessionId(sessionId); - } - } - - @Override - @Nullable - public String getSessionId() { - if (mKitManager != null) { - return mKitManager.getSessionId(); - } - return null; - } - static class CoreCallbacksImpl implements CoreCallbacks { KitFrameworkWrapper mKitFrameworkWrapper; ConfigManager mConfigManager; @@ -852,4 +792,4 @@ public void onKitApiCalled(String methodName, int kitId, Boolean used, Object... } }; } -} \ No newline at end of file +} diff --git a/android-core/src/main/java/com/mparticle/internal/KitManager.java b/android-core/src/main/java/com/mparticle/internal/KitManager.java index ad5c9808d..b821bdd73 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitManager.java +++ b/android-core/src/main/java/com/mparticle/internal/KitManager.java @@ -16,14 +16,10 @@ import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; import com.mparticle.WrapperSdkVersion; import com.mparticle.consent.ConsentState; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -33,8 +29,6 @@ import java.util.Map; import java.util.Set; -import kotlinx.coroutines.flow.Flow; - public interface KitManager { WeakReference getCurrentActivity(); @@ -133,41 +127,19 @@ public interface KitManager { void reset(); - void execute(@NonNull String identifier, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> embeddedViews, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config); - - Flow events(@NonNull String identifier); - - void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion); - - void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status); - - void close(); - - /** - * Set the session id to use for the next execute call. - * - * @param sessionId The session id to be set. Must be a non-empty string. - */ - void setSessionId(@NonNull String sessionId); - /** - * Get the session id to use within a non-native integration e.g. WebView. + * Get the RoktKitApi implementation if available. * - * @return The session id or null if no session is present. + * @return RoktKitApi instance or null if Rokt Kit is not configured or active */ @Nullable - String getSessionId(); + RoktKitApi getRoktKitApi(); - void prepareAttributesAsync(@NonNull Map attributes); + void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion); enum KitStatus { NOT_CONFIGURED, STOPPED, ACTIVE } -} \ No newline at end of file +} diff --git a/android-core/src/main/kotlin/com/mparticle/Rokt.kt b/android-core/src/main/kotlin/com/mparticle/Rokt.kt index 6b095b5e0..e531b7f44 100644 --- a/android-core/src/main/kotlin/com/mparticle/Rokt.kt +++ b/android-core/src/main/kotlin/com/mparticle/Rokt.kt @@ -3,7 +3,9 @@ package com.mparticle import android.graphics.Typeface import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager +import com.mparticle.internal.Logger import com.mparticle.internal.listeners.ApiClass +import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView import kotlinx.coroutines.flow.Flow @@ -12,6 +14,17 @@ import java.lang.ref.WeakReference @ApiClass class Rokt internal constructor(private val mConfigManager: ConfigManager, private val mKitManager: KitManager) { + + /** + * Display a Rokt placement with the specified parameters. + * + * @param identifier The placement identifier + * @param attributes User attributes to pass to Rokt + * @param callbacks Optional callback for Rokt events + * @param embeddedViews Optional map of embedded view placeholders + * @param fontTypefaces Optional map of font typefaces + * @param config Optional Rokt configuration + */ @JvmOverloads fun selectPlacements( identifier: String, @@ -22,25 +35,46 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva config: RoktConfig? = null, ) { if (mConfigManager.isEnabled) { - mKitManager.execute(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config) + val roktApi = mKitManager.roktKitApi + if (roktApi != null) { + roktApi.selectPlacements(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config, buildPlacementOptions()) + } else { + Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.") + } } } + /** + * Get a Flow of Rokt events for the specified identifier. + * + * @param identifier The placement identifier to listen for events + * @return A Flow emitting RoktEvent objects + */ fun events(identifier: String): Flow = if (mConfigManager.isEnabled) { - mKitManager.events(identifier) + mKitManager.roktKitApi?.events(identifier) ?: flowOf() } else { flowOf() } + /** + * Notify Rokt that a purchase has been finalized. + * + * @param placementId The placement identifier + * @param catalogItemId The catalog item identifier + * @param status Whether the purchase was successful + */ fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { if (mConfigManager.isEnabled) { - mKitManager.purchaseFinalized(placementId, catalogItemId, status) + mKitManager.roktKitApi?.purchaseFinalized(placementId, catalogItemId, status) } } + /** + * Close any active Rokt placements. + */ fun close() { if (mConfigManager.isEnabled) { - mKitManager.close() + mKitManager.roktKitApi?.close() } } @@ -56,7 +90,7 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva */ fun setSessionId(sessionId: String) { if (mConfigManager.isEnabled) { - mKitManager.setSessionId(sessionId) + mKitManager.roktKitApi?.setSessionId(sessionId) } } @@ -66,8 +100,23 @@ class Rokt internal constructor(private val mConfigManager: ConfigManager, priva * @return The session id or null if no session is present or SDK is not initialized. */ fun getSessionId(): String? = if (mConfigManager.isEnabled) { - mKitManager.getSessionId() + mKitManager.roktKitApi?.getSessionId() } else { null } + + /** + * Prepare attributes asynchronously before executing a placement. + * + * @param attributes The attributes to prepare + */ + fun prepareAttributesAsync(attributes: Map) { + if (mConfigManager.isEnabled) { + mKitManager.roktKitApi?.prepareAttributesAsync(attributes) + } + } + + private fun buildPlacementOptions(): PlacementOptions = PlacementOptions( + jointSdkSelectPlacements = System.currentTimeMillis(), + ) } diff --git a/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt b/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt new file mode 100644 index 000000000..68f9c628d --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt @@ -0,0 +1,87 @@ +package com.mparticle.internal + +import android.graphics.Typeface +import com.mparticle.MpRoktEventCallback +import com.mparticle.RoktEvent +import com.mparticle.rokt.PlacementOptions +import com.mparticle.rokt.RoktConfig +import com.mparticle.rokt.RoktEmbeddedView +import kotlinx.coroutines.flow.Flow +import java.lang.ref.WeakReference + +/** + * Interface for Rokt Kit operations. + * + * Implementations of this interface are provided by the Rokt Kit when it is + * configured and active. Use [KitManager.getRoktKitApi] to obtain an instance. + */ +interface RoktKitApi { + /** + * Initiate a Rokt placement selection with the specified parameters. + * + * @param viewName The identifier for the placement view + * @param attributes User attributes to pass to Rokt + * @param mpRoktEventCallback Optional callback for Rokt events + * @param placeHolders Optional map of embedded view placeholders + * @param fontTypefaces Optional map of font typefaces + * @param config Optional Rokt configuration + * @param options Optional placement options + */ + fun selectPlacements( + viewName: String, + attributes: Map, + mpRoktEventCallback: MpRoktEventCallback?, + placeHolders: Map>?, + fontTypefaces: Map>?, + config: RoktConfig?, + options: PlacementOptions? = null, + ) + + /** + * Get a Flow of Rokt events for the specified identifier. + * + * @param identifier The placement identifier to listen for events + * @return A Flow emitting RoktEvent objects + */ + fun events(identifier: String): Flow + + /** + * Notify Rokt that a purchase has been finalized. + * + * @param placementId The placement identifier + * @param catalogItemId The catalog item identifier + * @param status Whether the purchase was successful + */ + fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) + + /** + * Close any active Rokt placements. + */ + fun close() + + /** + * Set the session id to use for the next execute call. + * + * This is useful for cases where you have a session id from a non-native integration, + * e.g. WebView, and you want the session to be consistent across integrations. + * + * **Note:** Empty strings are ignored and will not update the session. + * + * @param sessionId The session id to be set. Must be a non-empty string. + */ + fun setSessionId(sessionId: String) + + /** + * Get the session id to use within a non-native integration e.g. WebView. + * + * @return The session id or null if no session is present. + */ + fun getSessionId(): String? + + /** + * Prepare attributes asynchronously before executing a placement. + * + * @param attributes The attributes to prepare + */ + fun prepareAttributesAsync(attributes: Map) +} diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt new file mode 100644 index 000000000..c3f2a3417 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt @@ -0,0 +1,3 @@ +package com.mparticle.rokt + +data class PlacementOptions(val jointSdkSelectPlacements: Long, val dynamicPerformanceMarkers: Map = mapOf()) diff --git a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt index aa2878fa6..317f58626 100644 --- a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt @@ -5,6 +5,8 @@ import android.os.Looper import android.os.SystemClock import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager +import com.mparticle.internal.RoktKitApi +import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView import kotlinx.coroutines.flow.Flow @@ -14,7 +16,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify @@ -33,10 +37,34 @@ class RoktTest { @Mock lateinit var kitManager: KitManager + @Mock + lateinit var roktKitApi: RoktKitApi + @Mock lateinit var configManager: ConfigManager private lateinit var rokt: Rokt + // Helpers to make Mockito matchers work in Kotlin with non-nullable types. + // Mockito matchers return null, which Kotlin rejects for non-nullable params. + // These helpers call the matcher (to register it) then return a cast null. + private fun capture(captor: ArgumentCaptor): T { + captor.capture() + @Suppress("UNCHECKED_CAST") + return null as T + } + + private fun any(): T { + ArgumentMatchers.any() + @Suppress("UNCHECKED_CAST") + return null as T + } + + private fun eq(value: T): T { + ArgumentMatchers.eq(value) + @Suppress("UNCHECKED_CAST") + return null as T + } + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -46,6 +74,7 @@ class RoktTest { @Test fun testSelectPlacements_withFullParams_whenEnabled() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val attributes = mutableMapOf() attributes["key"] = "value" @@ -82,31 +111,49 @@ class RoktTest { config = config, ) - verify(kitManager)?.execute("testView", attributes, callbacks, placeholders, fonts, config) + verify(roktKitApi).selectPlacements( + eq("testView"), + eq(attributes), + any(), + any(), + any(), + any(), + any(), + ) } @Test fun testSelectPlacements_withBasicParams_whenEnabled() { `when`(configManager.isEnabled()).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val attributes = mutableMapOf() attributes["a"] = "b" rokt.selectPlacements(attributes = attributes, identifier = "basicView") - verify(kitManager).execute("basicView", attributes, null, null, null, null) + verify(roktKitApi).selectPlacements( + eq("basicView"), + eq(attributes), + isNull(), + isNull(), + isNull(), + isNull(), + any(), + ) } @Test fun testSelectPlacements_withBasicParams_whenDisabled() { `when`(configManager.isEnabled()).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.selectPlacements( identifier = "basicView", attributes = HashMap(), ) - verify(kitManager, never()).execute(any(), any(), any(), any(), any(), any()) + verify(roktKitApi, never()).selectPlacements(any(), any(), any(), any(), any(), any(), any()) } @Test @@ -124,47 +171,51 @@ class RoktTest { @Test fun testReportConversion_withBasicParams_whenEnabled() { `when`(configManager.isEnabled()).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val attributes = mutableMapOf() attributes["a"] = "b" rokt.purchaseFinalized("132", "1111", true) - verify(kitManager).purchaseFinalized("132", "1111", true) + verify(roktKitApi).purchaseFinalized("132", "1111", true) } @Test fun testReportConversion_withBasicParams_whenDisabled() { `when`(configManager.isEnabled()).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.purchaseFinalized("132", "1111", true) - verify(kitManager, never()).purchaseFinalized("132", "1111", true) + verify(roktKitApi, never()).purchaseFinalized("132", "1111", true) } @Test fun testEvents_whenEnabled_delegatesToKitManager() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val testIdentifier = "test-identifier" val expectedFlow: Flow = flowOf() - `when`(kitManager.events(testIdentifier)).thenReturn(expectedFlow) + `when`(roktKitApi.events(testIdentifier)).thenReturn(expectedFlow) val result = rokt.events(testIdentifier) - verify(kitManager).events(testIdentifier) + verify(roktKitApi).events(testIdentifier) assertEquals(expectedFlow, result) } @Test fun testEvents_whenDisabled_returnsEmptyFlow() { `when`(configManager.isEnabled).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val testIdentifier = "test-identifier" val result = rokt.events(testIdentifier) - verify(kitManager, never()).events(any()) + verify(roktKitApi, never()).events(any()) runTest { val elements = result.toList() assertTrue(elements.isEmpty()) @@ -174,31 +225,63 @@ class RoktTest { @Test fun testSetSessionId_whenEnabled_delegatesToKitManager() { `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.setSessionId("test-session-id") - verify(kitManager).setSessionId("test-session-id") + verify(roktKitApi).setSessionId("test-session-id") } @Test fun testSetSessionId_whenDisabled_doesNotCallKitManager() { `when`(configManager.isEnabled).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) rokt.setSessionId("test-session-id") - verify(kitManager, never()).setSessionId(any()) + verify(roktKitApi, never()).setSessionId(any()) } @Test fun testGetSessionId_whenEnabled_delegatesToKitManager() { `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.getSessionId()).thenReturn("expected-session-id") + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) + `when`(roktKitApi.getSessionId()).thenReturn("expected-session-id") val result = rokt.getSessionId() - verify(kitManager).getSessionId() + verify(roktKitApi).getSessionId() assertEquals("expected-session-id", result) } @Test fun testGetSessionId_whenDisabled_returnsNull() { `when`(configManager.isEnabled).thenReturn(false) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) val result = rokt.getSessionId() - verify(kitManager, never()).getSessionId() + verify(roktKitApi, never()).getSessionId() assertNull(result) } + + @Test + fun testSelectPlacements_withOptions_whenEnabled() { + `when`(configManager.isEnabled).thenReturn(true) + `when`(kitManager.roktKitApi).thenReturn(roktKitApi) + val currentTimeMillis = System.currentTimeMillis() + + val attributes = mutableMapOf() + + rokt.selectPlacements( + identifier = "testView", + attributes = attributes, + ) + + // Verify call is forwarded + val viewNameCaptor = ArgumentCaptor.forClass(String::class.java) + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktKitApi).selectPlacements( + eq("testView"), + any(), + isNull(), + isNull(), + isNull(), + isNull(), + capture(optionsCaptor), + ) + assertTrue(optionsCaptor.value.jointSdkSelectPlacements >= currentTimeMillis) + } } diff --git a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt index 6e5126bc7..f1b327c9d 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt @@ -8,16 +8,11 @@ import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.MParticleOptions import com.mparticle.MockMParticle -import com.mparticle.RoktEvent import com.mparticle.WrapperSdk import com.mparticle.WrapperSdkVersion import com.mparticle.commerce.CommerceEvent import com.mparticle.internal.PushRegistrationHelper.PushRegistration import com.mparticle.testutils.RandomUtils -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest import org.json.JSONArray import org.junit.Assert import org.junit.Test @@ -34,7 +29,7 @@ import org.powermock.modules.junit4.PowerMockRunner import java.lang.ref.WeakReference import java.util.Random import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.test.assertNull @RunWith(PowerMockRunner::class) class KitFrameworkWrapperTest { @@ -728,7 +723,7 @@ class KitFrameworkWrapperTest { } @Test - fun testEvents_kitManagerNull_returnsEmptyFlow() { + fun testGetRoktKitApi_kitManagerNull_returnsNull() { val wrapper = KitFrameworkWrapper( mock( @@ -741,16 +736,13 @@ class KitFrameworkWrapperTest { mock(MParticleOptions::class.java), ) - val result = wrapper.events("test-identifier") + val result = wrapper.roktKitApi - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } + assertNull(result) } @Test - fun testEvents_kitManagerSet_delegatesToKitManager() { + fun testGetRoktKitApi_kitManagerSet_delegatesToKitManager() { val wrapper = KitFrameworkWrapper( mock( @@ -764,15 +756,14 @@ class KitFrameworkWrapperTest { ) val mockKitManager = mock(KitManager::class.java) - val expectedFlow: Flow = flowOf() - val testIdentifier = "test-identifier" + val mockRoktKitApi = mock(RoktKitApi::class.java) - `when`(mockKitManager.events(testIdentifier)).thenReturn(expectedFlow) + `when`(mockKitManager.roktKitApi).thenReturn(mockRoktKitApi) wrapper.setKitManager(mockKitManager) - val result = wrapper.events(testIdentifier) + val result = wrapper.roktKitApi - verify(mockKitManager).events(testIdentifier) - assertEquals(expectedFlow, result) + verify(mockKitManager).roktKitApi + assertEquals(mockRoktKitApi, result) } } diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java index a542373e1..206c108f5 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java @@ -21,6 +21,7 @@ import com.mparticle.commerce.CommerceEvent; import com.mparticle.consent.ConsentState; import com.mparticle.identity.MParticleUser; +import com.mparticle.rokt.PlacementOptions; import com.mparticle.rokt.RoktConfig; import com.mparticle.rokt.RoktEmbeddedView; @@ -115,7 +116,7 @@ public boolean isDisabled() { public boolean isDisabled(boolean isOptOutEvent) { return !getConfiguration().passesBracketing(kitManager.getUserBucket()) || - (getConfiguration().shouldHonorOptOut() && kitManager.isOptedOut() && !isOptOutEvent); + (getConfiguration().shouldHonorOptOut() && kitManager.isOptedOut() && !isOptOutEvent); } @Deprecated @@ -141,7 +142,7 @@ public final Map getUserIdentities() { /** * Retrieve filtered user attributes. Use this method to retrieve user attributes at any time. * To ensure that filtering is respected, kits must use this method rather than the public API. - * + *

* If the KitIntegration implements the {@link AttributeListener} interface and returns true * for {@link AttributeListener#supportsAttributeLists()}, this method will pass back all attributes * as they are (as String values or as List<String> values). Otherwise, this method will comma-separate @@ -160,8 +161,8 @@ public final Map getAllUserAttributes() { userAttributes = kitManager.getDataplanFilter().transformUserAttributes(userAttributes); } Map attributes = (Map) KitConfiguration.filterAttributes( - getConfiguration().getUserAttributeFilters(), - userAttributes + getConfiguration().getUserAttributeFilters(), + userAttributes ); if ((this instanceof AttributeListener) && ((AttributeListener) this).supportsAttributeLists()) { return attributes; @@ -418,7 +419,7 @@ public interface AttributeListener { /** * Indicate to the mParticle Kit framework if this AttributeListener supports attribute-values as lists. - * + *

* If an AttributeListener returns false, the setUserAttributeList method will never be called. Instead, setUserAttribute * will be called with the attribute-value lists combined as a csv. * @@ -617,14 +618,26 @@ public interface BatchListener { List logBatch(JSONObject jsonObject); } + /** + * Interface for Rokt Kit implementations. + * + *

This interface is internal to kit-base and is bridged to the + * {@link com.mparticle.internal.RoktKitApi} interface via a wrapper implementation + * in {@link KitManagerImpl}. The wrapper handles user resolution and + * attribute preparation before delegating to the kit's methods.

+ * + * @see com.mparticle.internal.RoktKitApi + */ public interface RoktListener { - void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable FilteredMParticleUser user, - @Nullable RoktConfig config); + + void selectPlacements(@NonNull String viewName, + @NonNull Map attributes, + @Nullable MpRoktEventCallback mpRoktEventCallback, + @Nullable Map> placeHolders, + @Nullable Map> fontTypefaces, + @Nullable FilteredMParticleUser user, + @Nullable RoktConfig config, + @Nullable PlacementOptions options); Flow events(@NonNull String identifier); diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java index cb9922da0..ef151ee9e 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java @@ -25,19 +25,14 @@ import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.MParticleTask; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; +import com.mparticle.internal.RoktKitApi; import com.mparticle.UserAttributeListener; import com.mparticle.WrapperSdkVersion; import com.mparticle.commerce.CommerceEvent; import com.mparticle.consent.ConsentState; -import com.mparticle.identity.IdentityApi; import com.mparticle.identity.IdentityApiRequest; -import com.mparticle.identity.IdentityApiResult; import com.mparticle.identity.IdentityStateListener; import com.mparticle.identity.MParticleUser; -import com.mparticle.internal.Constants; import com.mparticle.internal.CoreCallbacks; import com.mparticle.internal.KitManager; import com.mparticle.internal.KitsLoadedCallback; @@ -45,8 +40,6 @@ import com.mparticle.internal.MPUtility; import com.mparticle.internal.ReportingManager; import com.mparticle.kits.mappings.CustomMapping; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -62,13 +55,10 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -import kotlinx.coroutines.flow.Flow; - public class KitManagerImpl implements KitManager, AttributionListener, UserAttributeListener, IdentityStateListener { private static HandlerThread kitHandlerThread; @@ -1347,104 +1337,16 @@ public void reset() { } @Override - public void execute(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable RoktConfig config) { + @Nullable + public RoktKitApi getRoktKitApi() { for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - if (attributes == null) { - attributes = new HashMap<>(); - } - MParticle instance = MParticle.getInstance(); - MParticleUser user = instance.Identity().getCurrentUser(); - String email = getValueIgnoreCase(attributes, "email"); - String hashedEmail = getValueIgnoreCase(attributes, "emailsha256"); - Map tempAttributes = attributes; - KitConfiguration kitConfig = provider.getConfiguration(); - confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig, () -> { - Map finalAttributes = prepareAttributes(provider, tempAttributes, user); - - ((KitIntegration.RoktListener) provider).execute(viewName, - finalAttributes, - mpRoktEventCallback, - placeHolders, - fontTypefaces, - FilteredMParticleUser.getInstance(user.getId(), provider), - config); - }); - } - } catch (Exception e) { - Logger.warning("Failed to call execute for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - private String getValueIgnoreCase(Map map, String searchKey) { - for (Map.Entry entry : map.entrySet()) { - if (entry.getKey().equalsIgnoreCase(searchKey)) { - return entry.getValue(); + if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { + return new RoktKitApiImpl((KitIntegration.RoktListener) provider, provider); } } return null; } - private Map prepareAttributes(KitIntegration provider, Map finalAttributes, MParticleUser user){ - JSONArray jsonArray = new JSONArray(); - - KitConfiguration kitConfig = provider.getConfiguration(); - if (kitConfig != null) { - try { - jsonArray = kitConfig.getPlacementAttributesMapping(); - } catch (JSONException e) { - Logger.warning("Invalid placementAttributes for kit: " + provider.getName() + " JSON: " + e.getMessage()); - } - } - for (int i = 0; i < jsonArray.length(); i++) { - JSONObject obj = jsonArray.optJSONObject(i); - if (obj == null) continue; - String mapFrom = obj.optString("map"); - String mapTo = obj.optString("value"); - if (finalAttributes.containsKey(mapFrom)) { - String value = finalAttributes.remove(mapFrom); - finalAttributes.put(mapTo, value); - } - } - Map objectAttributes = new HashMap<>(); - for (Map.Entry entry : finalAttributes.entrySet()) { - if(!entry.getKey().equals(Constants.MessageKey.SANDBOX_MODE_ROKT)) { - objectAttributes.put(entry.getKey(), entry.getValue()); - } - } - if (user != null) { - user.setUserAttributes(objectAttributes); - } - - if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) { - finalAttributes.put(Constants.MessageKey.SANDBOX_MODE_ROKT, String.valueOf(Objects.toString(MPUtility.isDevEnv(), "false"))); // Default value is "false" if null - } - return finalAttributes; - } - - @Override - public Flow events(@NonNull String identifier) { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - Logger.verbose("Calling events for kit: " + provider.getName() + " with identifier: " + identifier); - return ((KitIntegration.RoktListener) provider).events(identifier); - } - } catch (Exception e) { - Logger.warning("Failed to call setWrapperSdkVersion for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - Logger.warning("No RoktListener found"); - return flowOf(); - } - @Override public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { for (KitIntegration provider : providers.values()) { @@ -1458,165 +1360,6 @@ public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { } } - @Override - public void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status) { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).purchaseFinalized(placementId,catalogItemId,status); - } - } catch (Exception e) { - Logger.warning("Failed to call purchaseFinalized for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - @Override - public void close() { - for (final KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).close(); - } - } catch (final Exception e) { - Logger.warning("Failed to call close for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - @Override - public void setSessionId(@NonNull String sessionId) { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).setSessionId(sessionId); - } - } catch (Exception e) { - Logger.warning("Failed to call setSessionId for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - @Override - @Nullable - public String getSessionId() { - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - return ((KitIntegration.RoktListener) provider).getSessionId(); - } - } catch (Exception e) { - Logger.warning("Failed to call getSessionId for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - return null; - } - - @Override - public void prepareAttributesAsync(@NonNull Map attributes) { - - for (KitIntegration provider : providers.values()) { - try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - if (attributes == null) { - attributes = new HashMap<>(); - } - MParticle instance = MParticle.getInstance(); - MParticleUser user = instance.Identity().getCurrentUser(); - String email = attributes.get("email"); - String hashedEmail = getValueIgnoreCase(attributes, "emailsha256"); - Map tempAttributes = attributes; - KitConfiguration kitConfig = provider.getConfiguration(); - confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig, () -> { - Map finalAttributes = prepareAttributes(provider, tempAttributes, user); - ((KitIntegration.RoktListener) provider).enrichAttributes( - finalAttributes, FilteredMParticleUser.getInstance(user.getId(), provider)); - }); - } - } catch (Exception e) { - Logger.warning("Failed to call prepareRoktListener for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - private void confirmEmail( - @Nullable String email, - @Nullable String hashedEmail, - @Nullable MParticleUser user, - IdentityApi identityApi, - KitConfiguration kitConfiguration, - Runnable runnable - ) { - boolean hasEmail = email != null && !email.isEmpty(); - boolean hasHashedEmail = hashedEmail != null && !hashedEmail.isEmpty(); - - if ((hasEmail || hasHashedEmail) && user != null) { - MParticle.IdentityType selectedIdentityType = null; - try { - String identityTypeStr = (kitConfiguration != null) - ? kitConfiguration.getHashedEmailUserIdentityType() - : null; - if (identityTypeStr != null ) { - selectedIdentityType = MParticle.IdentityType.valueOf(identityTypeStr); - } - } catch (IllegalArgumentException e) { - Logger.error("Invalid identity type "+e.getMessage()); - } - String existingEmail = user.getUserIdentities().get(MParticle.IdentityType.Email); - String existingHashedEmail = selectedIdentityType != null ? user.getUserIdentities().get(selectedIdentityType) : null; - boolean emailMismatch = hasEmail && !email.equalsIgnoreCase(existingEmail); - boolean hashedEmailMismatch = hasHashedEmail && !hashedEmail.equalsIgnoreCase(existingHashedEmail); - - if (emailMismatch || (hashedEmailMismatch && selectedIdentityType != null)) { - // If there's an existing email but it doesn't match the passed-in email, log a warning - if (emailMismatch && existingEmail != null) { - Logger.warning(String.format( - "The existing email on the user (%s) does not match the email passed to selectPlacements (%s). " + - "Please make sure to sync the email identity to mParticle as soon as it's available. " + - "Identifying user with the provided email before continuing to selectPlacements.", - existingEmail, email - )); - } - // If there's an existing other but it doesn't match the passed-in hashed email, log a warning - else if (hashedEmailMismatch && existingHashedEmail != null) { - Logger.warning(String.format( - "The existing hashed email on the user (%s) does not match the hashed email passed to selectPlacements (%s). " + - "Please make sure to sync the hashed email identity to mParticle as soon as it's available. " + - "Identifying user with the provided hashed email before continuing to selectPlacements.", - existingHashedEmail, hashedEmail - )); - } - - IdentityApiRequest.Builder identityBuilder = IdentityApiRequest.withUser(user); - if (emailMismatch) { - identityBuilder.email(email); - } - if (hashedEmailMismatch) { - identityBuilder.userIdentity(selectedIdentityType, hashedEmail); - } - - IdentityApiRequest identityRequest = identityBuilder.build(); - MParticleTask task = identityApi.identify(identityRequest); - - task.addFailureListener(result -> { - Logger.error("Failed to sync email from selectPlacement to user: " + result.getErrors()); - runnable.run(); - }); - - task.addSuccessListener(result -> { - Logger.debug("Updated email identity based on selectPlacement's attributes: " + - result.getUser().getUserIdentities().get(MParticle.IdentityType.Email)); - runnable.run(); - }); - - } else { - runnable.run(); - } - } else { - runnable.run(); - } - } - public void runOnKitThread(Runnable runnable) { if (mKitHandler == null) { mKitHandler = new Handler(kitHandlerThread.getLooper()); diff --git a/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt new file mode 100644 index 000000000..68fd73d4c --- /dev/null +++ b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt @@ -0,0 +1,257 @@ +package com.mparticle.kits + +import android.graphics.Typeface +import com.mparticle.MParticle +import com.mparticle.MpRoktEventCallback +import com.mparticle.RoktEvent +import com.mparticle.identity.IdentityApi +import com.mparticle.identity.IdentityApiRequest +import com.mparticle.identity.MParticleUser +import com.mparticle.internal.Constants +import com.mparticle.internal.Logger +import com.mparticle.internal.MPUtility +import com.mparticle.internal.RoktKitApi +import com.mparticle.rokt.PlacementOptions +import com.mparticle.rokt.RoktConfig +import com.mparticle.rokt.RoktEmbeddedView +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import org.json.JSONException +import java.lang.ref.WeakReference +import java.util.Objects + +/** + * Implementation of [RoktKitApi] that wraps a [KitIntegration.RoktListener]. + * + * This class handles user resolution and attribute preparation before delegating + * to the underlying Rokt Kit implementation. + */ +internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListener, private val kitIntegration: KitIntegration) : RoktKitApi { + + override fun selectPlacements( + viewName: String, + attributes: Map, + mpRoktEventCallback: MpRoktEventCallback?, + placeHolders: Map>?, + fontTypefaces: Map>?, + config: RoktConfig?, + options: PlacementOptions?, + ) { + try { + val mutableAttributes = attributes.toMutableMap() + val instance = MParticle.getInstance() + if (instance == null) { + Logger.warning("MParticle instance is null, cannot execute Rokt placement") + return + } + val user = instance.Identity().currentUser + val email = getValueIgnoreCase(mutableAttributes, "email") + val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256") + val kitConfig = kitIntegration.configuration + + confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) { + val finalAttributes = prepareAttributes(mutableAttributes, user) + roktListener.selectPlacements( + viewName, + finalAttributes, + mpRoktEventCallback, + placeHolders, + fontTypefaces, + FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration), + config, + options, + ) + } + } catch (e: Exception) { + Logger.warning("Failed to call execute for Rokt Kit: ${e.message}") + } + } + + override fun events(identifier: String): Flow = try { + Logger.verbose("Calling events for Rokt Kit with identifier: $identifier") + roktListener.events(identifier) + } catch (e: Exception) { + Logger.warning("Failed to call events for Rokt Kit: ${e.message}") + flowOf() + } + + override fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { + try { + roktListener.purchaseFinalized(placementId, catalogItemId, status) + } catch (e: Exception) { + Logger.warning("Failed to call purchaseFinalized for Rokt Kit: ${e.message}") + } + } + + override fun close() { + try { + roktListener.close() + } catch (e: Exception) { + Logger.warning("Failed to call close for Rokt Kit: ${e.message}") + } + } + + override fun setSessionId(sessionId: String) { + try { + roktListener.setSessionId(sessionId) + } catch (e: Exception) { + Logger.warning("Failed to call setSessionId for Rokt Kit: ${e.message}") + } + } + + override fun getSessionId(): String? = try { + roktListener.sessionId + } catch (e: Exception) { + Logger.warning("Failed to call getSessionId for Rokt Kit: ${e.message}") + null + } + + override fun prepareAttributesAsync(attributes: Map) { + try { + val mutableAttributes = attributes.toMutableMap() + val instance = MParticle.getInstance() + if (instance == null) { + Logger.warning("MParticle instance is null, cannot prepare attributes") + return + } + val user = instance.Identity().currentUser + val email = mutableAttributes["email"] + val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256") + val kitConfig = kitIntegration.configuration + + confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) { + val finalAttributes = prepareAttributes(mutableAttributes, user) + roktListener.enrichAttributes( + finalAttributes, + FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration), + ) + } + } catch (e: Exception) { + Logger.warning("Failed to call prepareAttributesAsync for Rokt Kit: ${e.message}") + } + } + + // Helper methods + + private fun getValueIgnoreCase(map: Map, searchKey: String): String? { + for ((key, value) in map) { + if (key.equals(searchKey, ignoreCase = true)) { + return value + } + } + return null + } + + private fun prepareAttributes(finalAttributes: MutableMap, user: MParticleUser?): MutableMap { + val kitConfig = kitIntegration.configuration + val jsonArray = try { + kitConfig?.placementAttributesMapping ?: org.json.JSONArray() + } catch (e: JSONException) { + Logger.warning("Invalid placementAttributes for Rokt Kit JSON: ${e.message}") + org.json.JSONArray() + } + + for (i in 0 until jsonArray.length()) { + val obj = jsonArray.optJSONObject(i) ?: continue + val mapFrom = obj.optString("map") + val mapTo = obj.optString("value") + if (finalAttributes.containsKey(mapFrom)) { + val value = finalAttributes.remove(mapFrom) + if (value != null) { + finalAttributes[mapTo] = value + } + } + } + + val objectAttributes = mutableMapOf() + for ((key, value) in finalAttributes) { + if (key != Constants.MessageKey.SANDBOX_MODE_ROKT) { + objectAttributes[key] = value + } + } + user?.setUserAttributes(objectAttributes) + + if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) { + finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] = + Objects.toString(MPUtility.isDevEnv(), "false") + } + return finalAttributes + } + + private fun confirmEmail( + email: String?, + hashedEmail: String?, + user: MParticleUser?, + identityApi: IdentityApi, + kitConfiguration: KitConfiguration?, + runnable: Runnable, + ) { + val hasEmail = !email.isNullOrEmpty() + val hasHashedEmail = !hashedEmail.isNullOrEmpty() + + if ((hasEmail || hasHashedEmail) && user != null) { + var selectedIdentityType: MParticle.IdentityType? = null + try { + val identityTypeStr = kitConfiguration?.hashedEmailUserIdentityType + if (identityTypeStr != null) { + selectedIdentityType = MParticle.IdentityType.valueOf(identityTypeStr) + } + } catch (e: IllegalArgumentException) { + Logger.error("Invalid identity type ${e.message}") + } + + val existingEmail = user.userIdentities[MParticle.IdentityType.Email] + val existingHashedEmail = selectedIdentityType?.let { user.userIdentities[it] } + val emailMismatch = hasEmail && !email.equals(existingEmail, ignoreCase = true) + val hashedEmailMismatch = + hasHashedEmail && !hashedEmail.equals(existingHashedEmail, ignoreCase = true) + + if (emailMismatch || (hashedEmailMismatch && selectedIdentityType != null)) { + // If there's an existing email but it doesn't match the passed-in email, log a warning + if (emailMismatch && existingEmail != null) { + Logger.warning( + "The existing email on the user ($existingEmail) does not match the email passed to selectPlacements ($email). " + + "Please make sure to sync the email identity to mParticle as soon as it's available. " + + "Identifying user with the provided email before continuing to selectPlacements.", + ) + } else if (hashedEmailMismatch && existingHashedEmail != null) { + // If there's an existing other but it doesn't match the passed-in hashed email, log a warning + Logger.warning( + "The existing hashed email on the user ($existingHashedEmail) does not match " + + "the hashed email passed to selectPlacements ($hashedEmail). " + + "Please make sure to sync the hashed email identity to mParticle as soon as it's available. " + + "Identifying user with the provided hashed email before continuing to selectPlacements.", + ) + } + + val identityBuilder = IdentityApiRequest.withUser(user) + if (emailMismatch) { + identityBuilder.email(email) + } + if (hashedEmailMismatch && selectedIdentityType != null) { + identityBuilder.userIdentity(selectedIdentityType, hashedEmail) + } + + val identityRequest = identityBuilder.build() + val task = identityApi.identify(identityRequest) + + task.addFailureListener { result -> + Logger.error("Failed to sync email from selectPlacement to user: ${result?.errors}") + runnable.run() + } + + task.addSuccessListener { result -> + Logger.debug( + "Updated email identity based on selectPlacement's attributes: " + + result.user.userIdentities[MParticle.IdentityType.Email], + ) + runnable.run() + } + } else { + runnable.run() + } + } else { + runnable.run() + } + } +} diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt index 1f1251038..b4001ba63 100644 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt @@ -8,7 +8,6 @@ import com.mparticle.BaseEvent import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.MParticleOptions -import com.mparticle.MParticleTask import com.mparticle.MpRoktEventCallback import com.mparticle.RoktEvent import com.mparticle.WrapperSdk @@ -18,7 +17,6 @@ import com.mparticle.commerce.Product import com.mparticle.consent.ConsentState import com.mparticle.consent.GDPRConsent import com.mparticle.identity.IdentityApi -import com.mparticle.identity.IdentityApiResult import com.mparticle.identity.MParticleUser import com.mparticle.internal.CoreCallbacks import com.mparticle.internal.Logger @@ -29,6 +27,7 @@ import com.mparticle.mock.MockContext import com.mparticle.mock.MockKitConfiguration import com.mparticle.mock.MockKitManagerImpl import com.mparticle.mock.MockMParticle +import com.mparticle.rokt.PlacementOptions import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView import com.mparticle.testutils.TestingUtils @@ -43,10 +42,12 @@ import org.json.JSONException import org.json.JSONObject import org.junit.Assert import org.junit.Assert.assertNull +import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.Mockito import org.mockito.Mockito.mock @@ -58,7 +59,6 @@ import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner import java.lang.ref.WeakReference -import java.lang.reflect.Method import java.util.Arrays import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap @@ -952,18 +952,23 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["no"]) - Assert.assertEquals("55555", attributes["minorcatid"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["no"]) + Assert.assertEquals("55555", finalAttributes["minorcatid"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test - fun testExecute_shouldNotModifyAttributes_ifMappedKeysDoNotExist() { + fun testSelectPlacements_shouldNotModifyAttributes_ifMappedKeysDoNotExist() { val sideloadedKit = mock(MPSideloadedKit::class.java) val kitId = 6000000 @@ -1025,19 +1030,24 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - - Assert.assertEquals("(123) 456-9898", attributes["call"]) - Assert.assertEquals("5-45555", attributes["postal"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + + Assert.assertEquals("(123) 456-9898", finalAttributes["call"]) + Assert.assertEquals("5-45555", finalAttributes["postal"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test - fun testExecute_shouldNotModifyAttributes_ifMapAndValueKeysAreSame() { + fun testSelectPlacements_shouldNotModifyAttributes_ifMapAndValueKeysAreSame() { val sideloadedKit = mock(MPSideloadedKit::class.java) val kitId = 6000000 @@ -1099,14 +1109,19 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["no"]) - Assert.assertEquals("5-45555", attributes["minorcatid"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["no"]) + Assert.assertEquals("5-45555", finalAttributes["minorcatid"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1170,613 +1185,19 @@ class KitManagerImplTest { Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) - } - - @Test - fun testConfirmEmail_When_EmailSyncSuccess() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "Test@gmail.com", "", user, identityApi, mockedKitConfig, runnable) - verify(mockTask).addSuccessListener(any()) - } - - @Test - fun testConfirmEmail_When_EmailAlreadySynced() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "Test@gmail.com", null, user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmEmail_When_mailIsNull() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, null, null, user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmEmail_When_User_IsNull() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldEmail = "Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Email, oldEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_EmailSyncSuccess() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_EmailAlreadySynced() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, null, "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_OtherIsNull() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_Test@gmail.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, null, null, user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testConfirmHashedEmail_When_HashedEmailUserIdentityType_Is_Other3() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - - `when`(mockedKitConfig.hashedEmailUserIdentityType).thenReturn("Other3") - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = hashMapOf( - "placementAttributesMapping" to """ - [ - // add placement attributes here if needed - ] - """.trimIndent(), - "hashedEmailUserIdentityType" to "Other3", - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = MParticleOptions.builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List).build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(mockTask).addSuccessListener(any()) - } - - @Test - fun testConfirmHashedEmail_When_HashedEmailUserIdentityType_Is_Unknown() { - var runnable: Runnable = mock(Runnable::class.java) - var user: MParticleUser = mock(MParticleUser::class.java) - val instance = MockMParticle() - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - - `when`(mockedKitConfig.hashedEmailUserIdentityType).thenReturn("Unknown") - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val identityApi = mock(IdentityApi::class.java) - val oldHashedEmail = "hashed_old@example.com" - val mockTask = mock(MParticleTask::class.java) as MParticleTask - `when`(identityApi.identify(any())).thenReturn(mockTask) - val identities: MutableMap = HashMap() - identities.put(MParticle.IdentityType.Other, oldHashedEmail) - `when`(user.userIdentities).thenReturn(identities) - instance.setIdentityApi(identityApi) - val settingsMap = hashMapOf( - "placementAttributesMapping" to """ - [ - // add placement attributes here if needed - ] - """.trimIndent(), - "hashedEmailUserIdentityType" to "Unknown", - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val options = MParticleOptions.builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List).build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = KitManagerImpl::class.java.getDeclaredMethod( - "confirmEmail", - String::class.java, - String::class.java, - MParticleUser::class.java, - IdentityApi::class.java, - KitConfiguration::class.java, - Runnable::class.java, - ) - method.isAccessible = true - val result = method.invoke(manager, "", "hashed_Test@gmail.com", user, identityApi, mockedKitConfig, runnable) - verify(runnable).run() - } - - @Test - fun testGetValueIgnoreCase_keyExistsDifferentCase() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = hashMapOf("Email" to "test@example.com") - val result = method.invoke(manager, map, "email") - assertEquals("test@example.com", result) - } - - @Test - fun testGetValueIgnoreCase_when__no_match() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = mapOf("Name" to "Test") - val result = method.invoke(manager, map, "email") - assertNull(result) - } - - @Test - fun testGetValueIgnoreCase_when_empty_map_returns_null() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = emptyMap() - val result = method.invoke(manager, map, "email") - assertNull(result) - } - - @Test - fun testGetValueIgnoreCase_when_empty_key_returns_null1() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - val manager: KitManagerImpl = MockKitManagerImpl(options) - val method: Method = - KitManagerImpl::class.java.getDeclaredMethod( - "getValueIgnoreCase", - Map::class.java, - String::class.java, - ) - method.isAccessible = true - val map = mapOf("Name" to "Test") - val result = method.invoke(manager, map, null) - assertNull(result) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -1838,14 +1259,94 @@ class KitManagerImplTest { Pair("customerId", "55555"), Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) + } + + @Test + fun testRokt_selectPlacements_with_PlacementOptions() { + val mockUser = mock(MParticleUser::class.java) + `when`(mockIdentity!!.currentUser).thenReturn(mockUser) + + val manager: KitManagerImpl = MockKitManagerImpl() + val roktListener = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(roktListener.isDisabled).thenReturn(false) + manager.providers = + ConcurrentHashMap().apply { + put(1, roktListener) + } + + val attributes = hashMapOf() + val placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) + + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null, placementOptions) + + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktListener as KitIntegration.RoktListener).selectPlacements( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + optionsCaptor.capture(), + ) + assertSame(placementOptions, optionsCaptor.value) + } + + @Test + fun testRokt_selectPlacements_without_PlacementOptions() { + val mockUser = mock(MParticleUser::class.java) + `when`(mockIdentity!!.currentUser).thenReturn(mockUser) + + val manager: KitManagerImpl = MockKitManagerImpl() + val roktListener = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(roktListener.isDisabled).thenReturn(false) + manager.providers = + ConcurrentHashMap().apply { + put(1, roktListener) + } + + val attributes = hashMapOf() + + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktListener as KitIntegration.RoktListener).selectPlacements( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + optionsCaptor.capture(), + ) + assertNull(optionsCaptor.value) } @Test @@ -1908,14 +1409,19 @@ class KitManagerImplTest { Pair("customerId", "55555"), Pair("country", "US"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("true", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("true", finalAttributes["sandbox"]) } @Test @@ -1979,14 +1485,19 @@ class KitManagerImplTest { Pair("country", "US"), Pair("sandbox", "false"), ) - manager.execute("Test", attributes, null, null, null, null) - Assert.assertEquals(6, attributes.size) - Assert.assertEquals("(123) 456-9898", attributes["number"]) - Assert.assertEquals("55555", attributes["customerId"]) - Assert.assertEquals("Test1", attributes["lastname"]) - Assert.assertEquals("Test", attributes["test"]) - Assert.assertEquals("US", attributes["country"]) - Assert.assertEquals("false", attributes["sandbox"]) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + roktApi!!.selectPlacements("Test", attributes, null, null, null, null) + val finalAttributes = mockedProvider.lastAttributes + Assert.assertNotNull(finalAttributes) + finalAttributes!! + Assert.assertEquals(6, finalAttributes.size) + Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) + Assert.assertEquals("55555", finalAttributes["customerId"]) + Assert.assertEquals("Test1", finalAttributes["lastname"]) + Assert.assertEquals("Test", finalAttributes["test"]) + Assert.assertEquals("US", finalAttributes["country"]) + Assert.assertEquals("false", finalAttributes["sandbox"]) } @Test @@ -2030,7 +1541,9 @@ class KitManagerImplTest { fun testEvents_noProviders_returnsEmptyFlow() { val manager: KitManagerImpl = MockKitManagerImpl() - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + assertNull(roktApi) + val result = roktApi?.events("test-identifier") ?: flowOf() runTest { val elements = result.toList() @@ -2051,7 +1564,9 @@ class KitManagerImplTest { put(1, nonRoktProvider) } - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + assertNull(roktApi) + val result = roktApi?.events("test-identifier") ?: flowOf() runTest { val elements = result.toList() @@ -2076,7 +1591,9 @@ class KitManagerImplTest { put(1, disabledRoktProvider) } - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + assertNull(roktApi) + val result = roktApi?.events("test-identifier") ?: flowOf() runTest { val elements = result.toList() @@ -2106,7 +1623,9 @@ class KitManagerImplTest { put(1, enabledRoktProvider) } - val result = manager.events(testIdentifier) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events(testIdentifier) verify(enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier) assertEquals(expectedFlow, result) @@ -2159,7 +1678,9 @@ class KitManagerImplTest { put(4, secondEnabledRoktProvider) } - val result = manager.events(testIdentifier) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events(testIdentifier) verify(enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier) verify(secondEnabledRoktProvider as KitIntegration.RoktListener, never()).events(any()) @@ -2185,7 +1706,9 @@ class KitManagerImplTest { put(1, exceptionRoktProvider) } - val result = manager.events("test-identifier") + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events("test-identifier") runTest { val elements = result.toList() @@ -2194,7 +1717,7 @@ class KitManagerImplTest { } @Test - fun testEvents_providerThrowsException_continuesToNextProvider() { + fun testEvents_providerThrowsException_returnsEmptyFlowWithoutFallback() { val manager: KitManagerImpl = MockKitManagerImpl() val exceptionRoktProvider = @@ -2226,16 +1749,25 @@ class KitManagerImplTest { put(2, workingRoktProvider) } - val result = manager.events(testIdentifier) + val roktApi = manager.getRoktKitApi() + Assert.assertNotNull(roktApi) + val result = roktApi!!.events(testIdentifier) - verify(workingRoktProvider as KitIntegration.RoktListener).events(testIdentifier) - assertEquals(expectedFlow, result) + verify(workingRoktProvider as KitIntegration.RoktListener, never()).events(any()) + runTest { + val elements = result.toList() + assertTrue(elements.isEmpty()) + } } internal inner class MockProvider( val config: KitConfiguration, ) : KitIntegration(), KitIntegration.RoktListener { + var lastAttributes: Map? = null + var lastOptions: PlacementOptions? = null + var lastUser: FilteredMParticleUser? = null + override fun isDisabled(): Boolean = false override fun getName(): String = "FakeProvider" @@ -2253,7 +1785,7 @@ class KitManagerImplTest { override fun getConfiguration(): KitConfiguration = config - override fun execute( + override fun selectPlacements( viewName: String, attributes: MutableMap, mpRoktEventCallback: MpRoktEventCallback?, @@ -2261,8 +1793,12 @@ class KitManagerImplTest { fontTypefaces: MutableMap>?, user: FilteredMParticleUser?, config: RoktConfig?, + options: PlacementOptions?, ) { - Logger.info("Executed with $attributes") + lastAttributes = attributes.toMap() + lastOptions = options + lastUser = user + Logger.info("selectPlacements with $attributes and options $options") } override fun events(identifier: String): Flow { diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt new file mode 100644 index 000000000..65643eba5 --- /dev/null +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt @@ -0,0 +1,167 @@ +package com.mparticle.kits + +import com.mparticle.MParticle +import com.mparticle.identity.IdentityApi +import com.mparticle.internal.MPUtility +import com.mparticle.mock.MockMParticle +import com.mparticle.rokt.PlacementOptions +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.Mockito.withSettings + +class RoktKitApiImplTest { + @Before + fun setUp() { + val identityApi = mock(IdentityApi::class.java) + val instance = MockMParticle() + instance.setIdentityApi(identityApi) + MParticle.setInstance(instance) + } + + @Test + fun testSelectPlacements_mapsAttributesAndAddsSandbox() { + val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) + val settingsMap = + hashMapOf( + "placementAttributesMapping" to + """ + [ + {"map": "number", "value": "no"}, + {"map": "customerId", "value": "minorcatid"} + ] + """.trimIndent(), + ) + val field = KitConfiguration::class.java.getDeclaredField("settings") + field.isAccessible = true + field.set(kitConfig, settingsMap) + + val kitIntegration = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(kitIntegration.configuration).thenReturn(kitConfig) + val roktListener = kitIntegration as KitIntegration.RoktListener + val roktApi = RoktKitApiImpl(roktListener, kitIntegration) + + val attributes = + hashMapOf( + "number" to "(123) 456-9898", + "customerId" to "55555", + "country" to "US", + ) + + roktApi.selectPlacements("Test", attributes, null, null, null, null, null) + + @Suppress("UNCHECKED_CAST") + val attributesCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor> + verify(roktListener).selectPlacements( + any(), + attributesCaptor.capture(), + any(), + any(), + any(), + any(), + any(), + any(), + ) + val captured = attributesCaptor.value + assertEquals("(123) 456-9898", captured["no"]) + assertEquals("55555", captured["minorcatid"]) + assertEquals("US", captured["country"]) + assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"]) + } + + @Test + fun testSelectPlacements_passesPlacementOptions() { + val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) + val kitIntegration = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(kitIntegration.configuration).thenReturn(kitConfig) + val roktListener = kitIntegration as KitIntegration.RoktListener + val roktApi = RoktKitApiImpl(roktListener, kitIntegration) + + val placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) + + roktApi.selectPlacements("Test", emptyMap(), null, null, null, null, placementOptions) + + val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) + verify(roktListener).selectPlacements( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + optionsCaptor.capture(), + ) + assertEquals(placementOptions, optionsCaptor.value) + } + + @Test + fun testEvents_returnsEmptyFlowWhenProviderThrows() = runTest { + val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) + val kitIntegration = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(kitIntegration.configuration).thenReturn(kitConfig) + val roktListener = kitIntegration as KitIntegration.RoktListener + `when`(roktListener.events(any())).thenThrow(RuntimeException("Test exception")) + val roktApi = RoktKitApiImpl(roktListener, kitIntegration) + + val result = roktApi.events("test-identifier") + + assertTrue(result.toList().isEmpty()) + } + + @Test + fun testPrepareAttributesAsync_delegatesToEnrichAttributes() { + val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) + val settingsMap = + hashMapOf( + "placementAttributesMapping" to + """ + [ + {"map": "number", "value": "no"} + ] + """.trimIndent(), + ) + val field = KitConfiguration::class.java.getDeclaredField("settings") + field.isAccessible = true + field.set(kitConfig, settingsMap) + + val kitIntegration = + mock( + KitIntegration::class.java, + withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), + ) + `when`(kitIntegration.configuration).thenReturn(kitConfig) + val roktListener = kitIntegration as KitIntegration.RoktListener + val roktApi = RoktKitApiImpl(roktListener, kitIntegration) + + roktApi.prepareAttributesAsync(mapOf("number" to "(123) 456-9898")) + + @Suppress("UNCHECKED_CAST") + val attributesCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor> + verify(roktListener).enrichAttributes(attributesCaptor.capture(), any()) + val captured = attributesCaptor.value + assertEquals("(123) 456-9898", captured["no"]) + assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"]) + } +}