diff --git a/build.gradle b/build.gradle index 8220292..ace5743 100644 --- a/build.gradle +++ b/build.gradle @@ -37,10 +37,17 @@ android { defaultConfig { minSdkVersion 19 } + testOptions { + unitTests.all { + jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + } + } } dependencies { - api 'com.appsflyer:af-android-sdk:6.16.0' + api 'com.appsflyer:af-android-sdk:6.17.3' + testImplementation files('libs/java-json.jar') + testImplementation files('libs/testutils.aar') } repositories { mavenCentral() diff --git a/libs/java-json.jar b/libs/java-json.jar new file mode 100755 index 0000000..2f211e3 Binary files /dev/null and b/libs/java-json.jar differ diff --git a/libs/test-utils.aar b/libs/test-utils.aar new file mode 100644 index 0000000..a13382c Binary files /dev/null and b/libs/test-utils.aar differ diff --git a/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt b/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt index 266f087..b9b3b92 100644 --- a/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt +++ b/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt @@ -16,6 +16,7 @@ import com.appsflyer.AFInAppEventType.ADD_TO_CART import com.appsflyer.AFInAppEventType.ADD_TO_WISH_LIST import com.appsflyer.AFInAppEventType.INITIATED_CHECKOUT import com.appsflyer.AFInAppEventType.PURCHASE +import com.appsflyer.AppsFlyerConsent import com.appsflyer.AppsFlyerConversionListener import com.appsflyer.AppsFlyerLib import com.appsflyer.AppsFlyerProperties @@ -27,8 +28,10 @@ import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.commerce.CommerceEvent import com.mparticle.commerce.Product +import com.mparticle.consent.ConsentState import com.mparticle.internal.Logger import com.mparticle.internal.MPUtility +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.math.BigDecimal @@ -40,7 +43,7 @@ import java.util.LinkedList */ class AppsFlyerKit : KitIntegration(), KitIntegration.EventListener, KitIntegration.AttributeListener, KitIntegration.CommerceListener, - AppsFlyerConversionListener, KitIntegration.ActivityListener { + AppsFlyerConversionListener, KitIntegration.ActivityListener, KitIntegration.UserAttributeListener { override fun getInstance(): AppsFlyerLib = AppsFlyerLib.getInstance(); @@ -54,6 +57,8 @@ class AppsFlyerKit : KitIntegration(), KitIntegration.EventListener, AppsFlyerLib.getInstance() .setDebugLog(MParticle.getInstance()?.environment == MParticle.Environment.Development) settings[DEV_KEY]?.let { AppsFlyerLib.getInstance().init(it, this, context) } + val userConsentState = currentUser?.consentState + setConsent(userConsentState) AppsFlyerLib.getInstance().start(context.applicationContext) AppsFlyerLib.getInstance().setCollectAndroidID(MParticle.isAndroidIdEnabled()) val integrationAttributes = HashMap(1) @@ -232,6 +237,37 @@ class AppsFlyerKit : KitIntegration(), KitIntegration.EventListener, override fun setUserAttribute(attributeKey: String, attributeValue: String) {} override fun setUserAttributeList(s: String, list: List) {} + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + user: FilteredMParticleUser? + ) { + } + + override fun onRemoveUserAttribute(key: String?, user: FilteredMParticleUser?) { + } + + override fun onSetUserAttribute(key: String?, value: Any?, user: FilteredMParticleUser?) { + } + + override fun onSetUserTag(key: String?, user: FilteredMParticleUser?) { + } + + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: MutableList?, + user: FilteredMParticleUser? + ) { + } + + override fun onSetAllUserAttributes( + userAttributes: MutableMap?, + userAttributeLists: MutableMap>?, + user: FilteredMParticleUser? + ) { + } + override fun supportsAttributeLists(): Boolean = true override fun setAllUserAttributes(map: Map, map1: Map>) {} override fun removeUserAttribute(key: String) {} @@ -257,6 +293,141 @@ class AppsFlyerKit : KitIntegration(), KitIntegration.EventListener, override fun logout(): List = emptyList() + private fun parseToNestedMap(jsonString: String): Map { + val topLevelMap = mutableMapOf() + try { + if (jsonString.isNullOrEmpty()) { + return topLevelMap + } + val jsonObject = JSONObject(jsonString) + + for (key in jsonObject.keys()) { + val value = jsonObject.get(key) + if (value is JSONObject) { + topLevelMap[key] = parseToNestedMap(value.toString()) + } else { + topLevelMap[key] = value + } + } + } catch (e: Exception) { + Logger.error( + e, + "The AppsFlyer kit was unable to parse the user's ConsentState, consent may not be set correctly on the AppsFlyer SDK" + ) + } + return topLevelMap + } + + private fun searchKeyInNestedMap(map: Map<*, *>, key: Any): Any? { + if (map.isNullOrEmpty()) { + return null + } + try { + for ((mapKey, mapValue) in map) { + if (mapKey.toString().equals(key.toString(), ignoreCase = true)) { + return mapValue + } + if (mapValue is Map<*, *>) { + val foundValue = searchKeyInNestedMap(mapValue, key) + if (foundValue != null) { + return foundValue + } + } + } + } catch (e: Exception) { + Logger.error( + e, + "The AppsFlyer kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status." + ) + } + return null + } + + override fun onConsentStateUpdated( + consentState: ConsentState, + consentState1: ConsentState, + filteredMParticleUser: FilteredMParticleUser + ) { + setConsent(consentState1) + } + + private fun setConsent(consentState: ConsentState?) { + if (settings[GDPR_APPLIES].isNullOrEmpty()) { + return + } + val appsFlyerGDPRUser: AppsFlyerConsent + if (!settings[GDPR_APPLIES].toBoolean()) { + appsFlyerGDPRUser = AppsFlyerConsent(false, null, null, null) + } else { + var adStorageConsentValue: Boolean? = null + when (settings[DEFAULT_AD_STORAGE_CONSENT]) { + AppsFlyerConsentValues.GRANTED.consentValue -> adStorageConsentValue = true + AppsFlyerConsentValues.DENIED.consentValue -> adStorageConsentValue = false + } + + var adUserDataConsentValue: Boolean? = null + when (settings[DEFAULT_AD_USER_DATA_CONSENT]) { + AppsFlyerConsentValues.GRANTED.consentValue -> adUserDataConsentValue = true + AppsFlyerConsentValues.DENIED.consentValue -> adUserDataConsentValue = false + } + + var adPersonalizationConsentValue: Boolean? = null + when (settings[DEFAULT_AD_PERSONALIZATION_CONSENT]) { + AppsFlyerConsentValues.GRANTED.consentValue -> adPersonalizationConsentValue = true + AppsFlyerConsentValues.DENIED.consentValue -> adPersonalizationConsentValue = false + } + + val clientConsentSettings = parseToNestedMap(consentState.toString()) + + parseConsentMapping(settings[consentMapping]).iterator().forEach { currentConsent -> + + val isConsentAvailable = + searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key) + + if (isConsentAvailable != null) { + val isConsentGranted: Boolean = + JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean + + when (currentConsent.value) { + "ad_storage" -> adStorageConsentValue = isConsentGranted + + "ad_user_data" -> adUserDataConsentValue = isConsentGranted + + "ad_personalization" -> adPersonalizationConsentValue = isConsentGranted + } + } + } + appsFlyerGDPRUser = AppsFlyerConsent(true, adUserDataConsentValue, adPersonalizationConsentValue, adStorageConsentValue) + } + AppsFlyerLib.getInstance().setConsentData(appsFlyerGDPRUser) + } + + private fun parseConsentMapping(json: String?): Map { + if (json.isNullOrEmpty()) { + return emptyMap() + } + val jsonWithFormat = json.replace("\\", "") + + return try { + JSONArray(jsonWithFormat) + .let { jsonArray -> + (0 until jsonArray.length()) + .associate { + val jsonObject = jsonArray.getJSONObject(it) + val map = jsonObject.getString("map") + val value = jsonObject.getString("value") + map to value + } + } + } catch (jse: JSONException) { + Logger.error( + jse, + "The AppsFlyer kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status." + ) + emptyMap() + } + } + override fun onConversionDataSuccess(conversionDataN: MutableMap?) { var conversionData = conversionDataN val jsonResult = JSONObject() @@ -389,5 +560,16 @@ class AppsFlyerKit : KitIntegration(), KitIntegration.EventListener, } else { null } } } + + private const val consentMapping = "consentMapping" + enum class AppsFlyerConsentValues(val consentValue: String) { + GRANTED("Granted"), + DENIED("Denied") + } + + val GDPR_APPLIES = "gdprApplies" + val DEFAULT_AD_STORAGE_CONSENT = "defaultAdStorageConsent" + val DEFAULT_AD_USER_DATA_CONSENT = "defaultAdUserDataConsent" + val DEFAULT_AD_PERSONALIZATION_CONSENT = "defaultAdPersonalizationConsent" } } diff --git a/src/test/kotlin/com/appsflyer/AppsFlyerConsent.kt b/src/test/kotlin/com/appsflyer/AppsFlyerConsent.kt new file mode 100644 index 0000000..c368b5c --- /dev/null +++ b/src/test/kotlin/com/appsflyer/AppsFlyerConsent.kt @@ -0,0 +1,10 @@ +package com.appsflyer + +class AppsFlyerConsent( + val isUserSubjectToGDPR: Boolean?, + val hasConsentForDataUsage: Boolean?, + val hasConsentForAdsPersonalization: Boolean?, + val hasConsentForAdStorage: Boolean? +) { + +} diff --git a/src/test/kotlin/com/appsflyer/AppsFlyerLib.kt b/src/test/kotlin/com/appsflyer/AppsFlyerLib.kt new file mode 100644 index 0000000..8406e93 --- /dev/null +++ b/src/test/kotlin/com/appsflyer/AppsFlyerLib.kt @@ -0,0 +1,51 @@ +package com.appsflyer + +import android.content.Context + +class AppsFlyerLib { + private var consentData: AppsFlyerConsent? = null + + fun setConsentData(consent: AppsFlyerConsent) { + consentData = consent + } + + fun getConsentData(): AppsFlyerConsent? { + return consentData + } + + fun getConsentState(): MutableMap { + val stateMap = mutableMapOf() + consentData?.let { consent -> + // Use property names directly instead of getter methods + consent.isUserSubjectToGDPR?.let { stateMap["isUserSubjectToGDPR"] = it } + consent.hasConsentForDataUsage?.let { stateMap["hasConsentForDataUsage"] = it } + consent.hasConsentForAdsPersonalization?.let { stateMap["hasConsentForAdsPersonalization"] = it } + consent.hasConsentForAdStorage?.let { stateMap["hasConsentForAdStorage"] = it } + } + return stateMap + } + + companion object { + private var _instance: AppsFlyerLib? = null + + @JvmStatic + fun getInstance(): AppsFlyerLib? { + if (_instance == null) { + _instance = AppsFlyerLib() + } + return _instance + } + + @JvmStatic + fun getInstance(context: Context?): AppsFlyerLib? { + return getInstance() + } + + /** + * Access Methods + */ + fun clearInstance() { + _instance = null + } + } +} diff --git a/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt b/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt index c28ee0f..9b1353d 100644 --- a/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt +++ b/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt @@ -1,20 +1,69 @@ package com.mparticle.kits +import android.app.Activity import android.content.Context +import android.net.Uri import com.mparticle.MParticle import com.mparticle.MParticleOptions import com.mparticle.commerce.CommerceEvent import com.mparticle.commerce.Product import com.mparticle.commerce.TransactionAttributes +import com.appsflyer.AppsFlyerLib +import com.mparticle.consent.ConsentState +import com.mparticle.consent.GDPRConsent +import com.mparticle.identity.IdentityApi +import com.mparticle.identity.MParticleUser +import com.mparticle.internal.CoreCallbacks +import com.mparticle.internal.CoreCallbacks.KitListener import junit.framework.Assert.assertEquals +import junit.framework.TestCase +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject import org.junit.Assert +import org.junit.Before import org.junit.Test +import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import java.lang.ref.WeakReference +import java.lang.reflect.Method class AppsflyerKitTests { - private val kit = AppsFlyerKit() + private var kit = AppsFlyerKit() + private var appsflyer = AppsFlyerLib() + + @Mock + lateinit var filteredMParticleUser: FilteredMParticleUser + + @Mock + lateinit var user: MParticleUser + + @Before + @Throws(JSONException::class) + fun before() { + AppsFlyerLib.clearInstance() + kit = AppsFlyerKit() + MockitoAnnotations.initMocks(this) + MParticle.setInstance(mock(MParticle::class.java)) + Mockito.`when`(MParticle.getInstance()?.Identity()).thenReturn( + mock( + IdentityApi::class.java + ) + ) + val kitManager = KitManagerImpl( + mock( + Context::class.java + ), null, emptyCoreCallbacks, mock(MParticleOptions::class.java) + ) + kit.kitManager = kitManager + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("id", "-1")) + appsflyer = AppsFlyerLib.getInstance(null)!! + } @Test @Throws(Exception::class) @@ -32,9 +81,9 @@ class AppsflyerKitTests { fun testOnKitCreate() { var e: Throwable? = null try { - val settings = HashMap() + val settings = HashMap() settings["fake setting"] = "fake" - kit.onKitCreate(settings as Map, Mockito.mock(Context::class.java)) + kit.onKitCreate(settings as Map, mock(Context::class.java)) } catch (ex: Throwable) { e = ex } @@ -44,7 +93,7 @@ class AppsflyerKitTests { @Test @Throws(Exception::class) fun testClassName() { - val options = Mockito.mock(MParticleOptions::class.java) + val options = mock(MParticleOptions::class.java) val factory = KitIntegrationFactory(options) val integrations = factory.supportedKits.values val className = kit.javaClass.name @@ -59,7 +108,7 @@ class AppsflyerKitTests { @Test @Throws(Exception::class) fun testGenerateSkuString() { - MParticle.setInstance(Mockito.mock(MParticle::class.java)) + MParticle.setInstance(mock(MParticle::class.java)) Mockito.`when`(MParticle.getInstance()?.environment) .thenReturn(MParticle.Environment.Production) Assert.assertNull(AppsFlyerKit.generateProductIdList(null)) @@ -74,13 +123,558 @@ class AppsflyerKitTests { .transactionAttributes(TransactionAttributes("foo")) .build() assertEquals( - mutableListOf("foo-sku","foo-sku-2"), AppsFlyerKit.generateProductIdList(event2)) + mutableListOf("foo-sku", "foo-sku-2"), AppsFlyerKit.generateProductIdList(event2) + ) val product3 = Product.Builder("foo-name-3", "foo-sku-,3", 50.0).build() val event3 = CommerceEvent.Builder(Product.PURCHASE, product) .addProduct(product2) .addProduct(product3) .transactionAttributes(TransactionAttributes("foo")) .build() - assertEquals(mutableListOf("foo-sku","foo-sku-2","foo-sku-%2C3"), AppsFlyerKit.generateProductIdList(event3)) + assertEquals( + mutableListOf("foo-sku", "foo-sku-2", "foo-sku-%2C3"), + AppsFlyerKit.generateProductIdList(event3) + ) + } + + @Test + @Throws(Exception::class) + fun testConsentWhenGDPRNotApplied() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "false" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Denied" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + + val marketingConsent = GDPRConsent.builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = ConsentState.builder() + .addGDPRConsentState("Marketing", marketingConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(false, expectedConsentValue) + + val notExpectedConsentKey = afConsentResults.containsKey("hasConsentForDataUsage") + TestCase.assertEquals(false, notExpectedConsentKey) + + val notExpectedConsentKey2 = afConsentResults.containsKey("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, notExpectedConsentKey2) + + val notExpectedConsentKey3 = afConsentResults.containsKey("hasConsentForAdStorage") + TestCase.assertEquals(false, notExpectedConsentKey3) + } + + @Test + @Throws(Exception::class) + fun testConsentWhenGDPRAppliedWithoutConsentDefaults() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Unspecified" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Unspecified" + map["defaultAdPersonalizationConsent"] = "Unspecified" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + + val marketingConsent = GDPRConsent.builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = ConsentState.builder() + .addGDPRConsentState("test1", marketingConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val notExpectedConsentKey = afConsentResults.containsKey("hasConsentForDataUsage") + TestCase.assertEquals(false, notExpectedConsentKey) + + val notExpectedConsentKey2 = afConsentResults.containsKey("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, notExpectedConsentKey2) + + val notExpectedConsentKey3 = afConsentResults.containsKey("hasConsentForAdStorage") + TestCase.assertEquals(false, notExpectedConsentKey3) + } + + @Test + @Throws(Exception::class) + fun testConsentWhenGDPRAppliedWithConsentDefaults() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Granted" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + + val marketingConsent = GDPRConsent.builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = ConsentState.builder() + .addGDPRConsentState("test1", marketingConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults.getValue("hasConsentForDataUsage") + TestCase.assertEquals(false, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults.getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(true, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults.getValue("hasConsentForAdStorage") + TestCase.assertEquals(true, expectedConsentValue4) + } + + @Test + @Throws(Exception::class) + fun testConsentMapping() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Granted" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + + val performanceConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val marketingConsent = GDPRConsent.builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val testConsent = GDPRConsent.builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = ConsentState.builder() + .addGDPRConsentState("Performance", performanceConsent) + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("testconsent", testConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults.getValue("hasConsentForDataUsage") + TestCase.assertEquals(true, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults.getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults.getValue("hasConsentForAdStorage") + TestCase.assertEquals(false, expectedConsentValue4) + } + + @Test + fun onConsentStateUpdatedTestPerformance_And_Marketing_are_true() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Granted" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + + val performanceConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val marketingConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = ConsentState.builder() + .addGDPRConsentState("Performance", performanceConsent) + .addGDPRConsentState("Marketing", marketingConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults.getValue("hasConsentForDataUsage") + TestCase.assertEquals(true, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults.getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(true, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults.getValue("hasConsentForAdStorage") + TestCase.assertEquals(true, expectedConsentValue4) + } + + @Test + fun onConsentStateUpdatedTest_When_No_Defaults_Values() { + val map = HashMap() + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + val marketingConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = GDPRConsent.builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = ConsentState.builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults.getValue("hasConsentForDataUsage") + TestCase.assertEquals(true, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults.getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(true, expectedConsentValue3) + + val notExpectedConsentKey = + afConsentResults.containsKey("hasConsentForAdStorage") + TestCase.assertEquals(false, notExpectedConsentKey) + } + + @Test + fun onConsentStateUpdatedTest_When_No_DATA_From_Server() { + + val marketingConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = GDPRConsent.builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = ConsentState.builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + TestCase.assertEquals(0, appsflyer.getConsentState().size) + } + + @Test + fun onConsentStateUpdatedTest_No_consentMappingSDK() { + val map = HashMap() + map["gdprApplies"] = "true" + map["defaultAdStorageConsent"] = "Granted" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Denied" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + val marketingConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = GDPRConsent.builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = ConsentState.builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults.getValue("hasConsentForDataUsage") + TestCase.assertEquals(false, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults.getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults.getValue("hasConsentForAdStorage") + TestCase.assertEquals(true, expectedConsentValue4) + + } + + @Test + fun onConsentStateUpdatedTest_When_default_is_Unspecified_And_No_consentMappingSDK_And_GDPR_Not_Applied() { + val map = HashMap() + map["gdprApplies"] = "false" + map["defaultAdStorageConsent"] = "Unspecified" + map["defaultAdUserDataConsent"] = "Unspecified" + map["defaultAdPersonalizationConsent"] = "Unspecified" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + val marketingConsent = GDPRConsent.builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = GDPRConsent.builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = ConsentState.builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + filteredMParticleUser = FilteredMParticleUser.getInstance(user, kit) + + kit.onConsentStateUpdated(state, state, filteredMParticleUser) + + TestCase.assertEquals(1, appsflyer.getConsentState().size) + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults.getValue("isUserSubjectToGDPR") + TestCase.assertEquals(false, expectedConsentValue) + } + + @Test + fun testParseToNestedMap_When_JSON_Is_INVALID() { + var jsonInput = + "{'GDPR':{'marketing':'{:false,'timestamp':1711038269644:'Test consent','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}','performance':'{'consented':true,'timestamp':1711038269644,'document':'parental_consent_agreement_v2','location':'17 Cherry Tree Lan 3','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'},'CCPA':'{'consented':true,'timestamp':1711038269644,'document':'ccpa_consent_agreement_v3','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}" + + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "parseToNestedMap", + String::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, jsonInput) + Assert.assertEquals(mutableMapOf(), result) + } + + @Test + fun testParseToNestedMap_When_JSON_Is_Empty() { + var jsonInput = "" + + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "parseToNestedMap", + String::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, jsonInput) + Assert.assertEquals(mutableMapOf(), result) + } + + @Test + fun testSearchKeyInNestedMap_When_Input_Key_Is_Empty_String() { + val map = mapOf( + "GDPR" to true, + "marketing" to mapOf( + "consented" to false, + "document" to mapOf( + "timestamp" to 1711038269644 + ) + ) + ) + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "searchKeyInNestedMap", Map::class.java, + Any::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, map, "") + Assert.assertEquals(null, result) + } + + @Test + fun testSearchKeyInNestedMap_When_Input_Is_Empty_Map() { + val emptyMap: Map = emptyMap() + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "searchKeyInNestedMap", Map::class.java, + Any::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, emptyMap, "1") + Assert.assertEquals(null, result) + } + + @Test + fun testParseConsentMapping_When_Input_Is_Empty_Json() { + val emptyJson = "" + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "parseConsentMapping", + String::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, emptyJson) + Assert.assertEquals(emptyMap(), result) + } + + @Test + fun testParseConsentMapping_When_Input_Is_Invalid_Json() { + var jsonInput = + "{'GDPR':{'marketing':'{:false,'timestamp':1711038269644:'Test consent','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}','performance':'{'consented':true,'timestamp':1711038269644,'document':'parental_consent_agreement_v2','location':'17 Cherry Tree Lan 3','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'},'CCPA':'{'consented':true,'timestamp':1711038269644,'document':'ccpa_consent_agreement_v3','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}" + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "parseConsentMapping", + String::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, jsonInput) + Assert.assertEquals(emptyMap(), result) + } + + @Test + fun testParseConsentMapping_When_Input_Is_NULL() { + val method: Method = AppsFlyerKit::class.java.getDeclaredMethod( + "parseConsentMapping", + String::class.java + ) + method.isAccessible = true + val result = method.invoke(kit, null) + Assert.assertEquals(emptyMap(), result) + } + + private var emptyCoreCallbacks: CoreCallbacks = object : CoreCallbacks { + var activity = Activity() + override fun isBackgrounded(): Boolean = false + + override fun getUserBucket(): Int = 0 + + override fun isEnabled(): Boolean = false + + override fun setIntegrationAttributes(i: Int, map: Map) {} + + override fun getIntegrationAttributes(i: Int): Map? = null + + override fun getCurrentActivity(): WeakReference = WeakReference(activity) + + override fun getLatestKitConfiguration(): JSONArray? = null + + override fun getDataplanOptions(): MParticleOptions.DataplanOptions? = null + + override fun isPushEnabled(): Boolean = false + + override fun getPushSenderId(): String? = null + + override fun getPushInstanceId(): String? = null + + override fun getLaunchUri(): Uri? = null + + override fun getLaunchAction(): String? = null + + override fun getKitListener(): KitListener { + return object : KitListener { + override fun kitFound(kitId: Int) {} + override fun kitConfigReceived(kitId: Int, configuration: String?) {} + override fun kitExcluded(kitId: Int, reason: String?) {} + override fun kitStarted(kitId: Int) {} + override fun onKitApiCalled(kitId: Int, used: Boolean?, vararg objects: Any?) {} + override fun onKitApiCalled( + methodName: String?, + kitId: Int, + used: Boolean?, + vararg objects: Any? + ) { + } + } + } + } } \ No newline at end of file