diff --git a/build.gradle b/build.gradle index c626387..7eb6be2 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,7 @@ dependencies { testImplementation files('libs/java-json.jar') testImplementation 'com.squareup.assertj:assertj-android:1.2.0' testImplementation ("io.mockk:mockk:1.13.4") + testImplementation 'org.mockito:mockito-inline:5.2.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' compileOnly 'androidx.compose.ui:ui' compileOnly 'androidx.compose.material:material' diff --git a/src/main/kotlin/com/mparticle/kits/RoktKit.kt b/src/main/kotlin/com/mparticle/kits/RoktKit.kt index 26c78a2..1bf54d2 100644 --- a/src/main/kotlin/com/mparticle/kits/RoktKit.kt +++ b/src/main/kotlin/com/mparticle/kits/RoktKit.kt @@ -38,6 +38,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.math.BigDecimal +import com.mparticle.internal.MPUtility +import com.mparticle.internal.MPUtility.AdIdInfo const val ROKT_ATTRIBUTE_SANDBOX_MODE: String = "sandbox" @@ -351,6 +353,15 @@ class RoktKit : } } } + + // GAID is only provided if advertising tracking is not limited + applicationContext?.let { context -> + val adIdInfo = MPUtility.getAdIdInfo(context) + if (adIdInfo != null && adIdInfo.advertiser == MPUtility.AdIdInfo.Advertiser.GOOGLE) { + identityAttributes[IDENTITY_TYPE_GAID] = adIdInfo.id + } + } + if (attributes != null) { attributes.putAll(identityAttributes) return attributes @@ -420,6 +431,7 @@ class RoktKit : const val ROKT_ACCOUNT_ID = "accountId" const val HASHED_EMAIL_USER_IDENTITY_TYPE = "hashedEmailUserIdentityType" const val MPID = "mpid" + const val IDENTITY_TYPE_GAID = "gaid" const val NO_ROKT_ACCOUNT_ID = "No Rokt account ID provided, can't initialize kit." const val NO_APP_VERSION_FOUND = "No App version found, can't initialize kit." } diff --git a/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt b/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt index 5dd583e..7305565 100644 --- a/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt +++ b/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt @@ -15,6 +15,7 @@ import com.mparticle.WrapperSdkVersion import com.mparticle.identity.IdentityApi import com.mparticle.internal.CoreCallbacks import com.mparticle.internal.CoreCallbacks.KitListener +import com.mparticle.internal.MPUtility import com.mparticle.kits.mocks.MockKitConfiguration import com.rokt.roktsdk.FulfillmentAttributes import com.rokt.roktsdk.Rokt @@ -36,11 +37,15 @@ import org.json.JSONArray import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.Mockito import org.mockito.Mockito.mock +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.`when` +import org.mockito.ArgumentMatchers.any as mockitoAny import java.lang.ref.WeakReference import java.lang.reflect.Field import java.lang.reflect.Method @@ -1148,6 +1153,210 @@ class RoktKitTests { assertEquals("allowed_value", result["allowed_key"]) } + @Test + fun testAddIdentityAttributes_adds_GAID_when_available() { + // Test that GAID is added when MPUtility.getAdIdInfo returns valid Google advertiser + + // Set applicationContext using reflection (required for GAID lookup) + val contextField = RoktKit::class.java.getDeclaredField("applicationContext") + contextField.isAccessible = true + contextField.set(roktKit, context) + + mockStatic(MPUtility::class.java).use { mockedMPUtility -> + // Create a mock AdIdInfo and set its fields using reflection + val mockAdIdInfo = mock(MPUtility.AdIdInfo::class.java) + + // Set the id field + val idField = MPUtility.AdIdInfo::class.java.getDeclaredField("id") + idField.isAccessible = true + idField.set(mockAdIdInfo, "test-gaid-12345") + + // Set the advertiser field + val advertiserField = MPUtility.AdIdInfo::class.java.getDeclaredField("advertiser") + advertiserField.isAccessible = true + advertiserField.set(mockAdIdInfo, MPUtility.AdIdInfo.Advertiser.GOOGLE) + + mockedMPUtility.`when` { + MPUtility.getAdIdInfo(mockitoAny(Context::class.java)) + }.thenReturn(mockAdIdInfo) + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + val userIdentities = HashMap() + userIdentities[IdentityType.Email] = "test@example.com" + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(userIdentities) + + val attributes: MutableMap = mutableMapOf("existing_key" to "existing_value") + + val method: Method = RoktKit::class.java.getDeclaredMethod( + "addIdentityAttributes", + MutableMap::class.java, + FilteredMParticleUser::class.java, + ) + method.isAccessible = true + + // Act + val result = method.invoke(roktKit, attributes, mockFilterUser) as Map + + // Assert + assertTrue("GAID should be present in result", result.containsKey("gaid")) + assertEquals("test-gaid-12345", result["gaid"]) + + // Verify user identities are still added + assertEquals("test@example.com", result["email"]) + + // Verify existing attributes are preserved + assertEquals("existing_value", result["existing_key"]) + } + } + + @Test + fun testAddIdentityAttributes_GAID_not_added_when_null() { + // Test that GAID is not added when MPUtility.getAdIdInfo returns null + + // Set applicationContext using reflection (required for GAID lookup) + val contextField = RoktKit::class.java.getDeclaredField("applicationContext") + contextField.isAccessible = true + contextField.set(roktKit, context) + + mockStatic(MPUtility::class.java).use { mockedMPUtility -> + // Mock getAdIdInfo to return null (ad tracking not available or not authorized) + mockedMPUtility.`when` { + MPUtility.getAdIdInfo(mockitoAny(Context::class.java)) + }.thenReturn(null) + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + val userIdentities = HashMap() + userIdentities[IdentityType.Email] = "test@example.com" + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(userIdentities) + + val attributes: MutableMap = mutableMapOf() + + val method: Method = RoktKit::class.java.getDeclaredMethod( + "addIdentityAttributes", + MutableMap::class.java, + FilteredMParticleUser::class.java, + ) + method.isAccessible = true + + // Act + val result = method.invoke(roktKit, attributes, mockFilterUser) as Map + + // Assert - GAID should NOT be present + assertFalse("GAID should not be present when getAdIdInfo returns null", result.containsKey("gaid")) + + // Other identities should still be added + assertTrue(result.containsKey("email")) + assertEquals("test@example.com", result["email"]) + } + } + + @Test + fun testAddIdentityAttributes_GAID_coexists_with_google_user_identity() { + // Test that GAID (gaid key) coexists with Google user identity (google key) + + // Set applicationContext using reflection (required for GAID lookup) + val contextField = RoktKit::class.java.getDeclaredField("applicationContext") + contextField.isAccessible = true + contextField.set(roktKit, context) + + mockStatic(MPUtility::class.java).use { mockedMPUtility -> + // Create a mock AdIdInfo and set its fields using reflection + val mockAdIdInfo = mock(MPUtility.AdIdInfo::class.java) + + val idField = MPUtility.AdIdInfo::class.java.getDeclaredField("id") + idField.isAccessible = true + idField.set(mockAdIdInfo, "device-gaid-value") + + val advertiserField = MPUtility.AdIdInfo::class.java.getDeclaredField("advertiser") + advertiserField.isAccessible = true + advertiserField.set(mockAdIdInfo, MPUtility.AdIdInfo.Advertiser.GOOGLE) + + mockedMPUtility.`when` { + MPUtility.getAdIdInfo(mockitoAny(Context::class.java)) + }.thenReturn(mockAdIdInfo) + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + val userIdentities = HashMap() + userIdentities[IdentityType.Email] = "test@example.com" + userIdentities[IdentityType.Google] = "user-google-identity" + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(userIdentities) + + val attributes: MutableMap = mutableMapOf() + + val method: Method = RoktKit::class.java.getDeclaredMethod( + "addIdentityAttributes", + MutableMap::class.java, + FilteredMParticleUser::class.java, + ) + method.isAccessible = true + + // Act + val result = method.invoke(roktKit, attributes, mockFilterUser) as Map + + // Assert - Both should be present with different keys and values + assertTrue(result.containsKey("google")) + assertEquals("user-google-identity", result["google"]) + + assertTrue(result.containsKey("gaid")) + assertEquals("device-gaid-value", result["gaid"]) + + // Verify they don't interfere with each other + assertNotEquals(result["google"], result["gaid"]) + } + } + + @Test + fun testAddIdentityAttributes_GAID_not_added_for_non_google_advertiser() { + // Test that GAID is NOT added when advertiser is not Google (e.g., Amazon Fire) + // Ensures we only add GAID for actual Google Advertising IDs + + // Set applicationContext using reflection (required for GAID lookup) + val contextField = RoktKit::class.java.getDeclaredField("applicationContext") + contextField.isAccessible = true + contextField.set(roktKit, context) + + mockStatic(MPUtility::class.java).use { mockedMPUtility -> + // Create a mock AdIdInfo with Amazon advertiser (not Google) + val mockAdIdInfo = mock(MPUtility.AdIdInfo::class.java) + + val idField = MPUtility.AdIdInfo::class.java.getDeclaredField("id") + idField.isAccessible = true + idField.set(mockAdIdInfo, "amazon-ad-id-12345") + + val advertiserField = MPUtility.AdIdInfo::class.java.getDeclaredField("advertiser") + advertiserField.isAccessible = true + advertiserField.set(mockAdIdInfo, MPUtility.AdIdInfo.Advertiser.AMAZON) + + mockedMPUtility.`when` { + MPUtility.getAdIdInfo(mockitoAny(Context::class.java)) + }.thenReturn(mockAdIdInfo) + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + val userIdentities = HashMap() + userIdentities[IdentityType.Email] = "test@example.com" + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(userIdentities) + + val attributes: MutableMap = mutableMapOf() + + val method: Method = RoktKit::class.java.getDeclaredMethod( + "addIdentityAttributes", + MutableMap::class.java, + FilteredMParticleUser::class.java, + ) + method.isAccessible = true + + // Act + val result = method.invoke(roktKit, attributes, mockFilterUser) as Map + + // Assert - GAID should NOT be added for Amazon advertiser + assertFalse("GAID should not be added for non-Google advertiser", result.containsKey("gaid")) + + // Other identities should still be added + assertTrue(result.containsKey("email")) + assertEquals("test@example.com", result["email"]) + } + } + internal inner class TestCoreCallbacks : CoreCallbacks { override fun isBackgrounded(): Boolean = false override fun getUserBucket(): Int = 0