diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8fb49df..83eb4a6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,10 +2,18 @@ version: 2 updates: - package-ecosystem: gradle directory: "/" - target-branch: "development" labels: [ "dependabot" ] schedule: interval: "daily" open-pull-requests-limit: 10 commit-message: prefix: "chore" +- package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 4 + labels: + - dependabot + commit-message: + prefix: "chore" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml deleted file mode 100644 index 4a67ce3..0000000 --- a/.github/workflows/sonarcloud.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "SonarCloud" - -on: - workflow_run: - workflows: ["Pull Request", "Push"] - types: - - completed - -jobs: - sonarcloud-check: - name: "SonarCloud Check" - uses: mParticle/mparticle-workflows/.github/workflows/sonarcloud.yml@main - secrets: inherit \ No newline at end of file diff --git a/build.gradle b/build.gradle index 41db8dc..71564e5 100644 --- a/build.gradle +++ b/build.gradle @@ -68,11 +68,11 @@ dependencies { implementation 'androidx.annotation:annotation:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' implementation 'androidx.compose.runtime:runtime' - api 'com.rokt:roktsdk:4.11.2' + api 'com.rokt:roktsdk:4.12.1' testImplementation files('libs/java-json.jar') testImplementation 'com.squareup.assertj:assertj-android:1.2.0' - testImplementation ("io.mockk:mockk:1.13.4") + testImplementation ("io.mockk:mockk:1.14.6") 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/RoktConfigExtensions.kt b/src/main/kotlin/com/mparticle/kits/RoktConfigExtensions.kt new file mode 100644 index 0000000..c072919 --- /dev/null +++ b/src/main/kotlin/com/mparticle/kits/RoktConfigExtensions.kt @@ -0,0 +1,32 @@ +package com.mparticle.kits + +import com.mparticle.rokt.RoktConfig +import com.mparticle.rokt.CacheConfig as MpCacheConfig +import com.rokt.roktsdk.CacheConfig +import com.rokt.roktsdk.RoktConfig as RoktSdkConfig + +fun MpCacheConfig.toRoktSdkCacheConfig(): CacheConfig = CacheConfig( + cacheDurationInSeconds = this.cacheDurationInSeconds, + cacheAttributes = this.cacheAttributes, +) + +fun RoktConfig.toRoktSdkConfig(): RoktSdkConfig { + val colorMode = when (this.colorMode) { + RoktConfig.ColorMode.LIGHT -> RoktSdkConfig.ColorMode.LIGHT + RoktConfig.ColorMode.DARK -> RoktSdkConfig.ColorMode.DARK + RoktConfig.ColorMode.SYSTEM -> RoktSdkConfig.ColorMode.SYSTEM + else -> RoktSdkConfig.ColorMode.SYSTEM + } + + val cacheConfig = this.cacheConfig?.toRoktSdkCacheConfig() + + val edgeToEdgeDisplay = this.edgeToEdgeDisplay + + val builder = RoktSdkConfig.Builder() + .colorMode(colorMode) + .edgeToEdgeDisplay(edgeToEdgeDisplay) + + cacheConfig?.let { builder.cacheConfig(it) } + + return builder.build() +} diff --git a/src/main/kotlin/com/mparticle/kits/RoktKit.kt b/src/main/kotlin/com/mparticle/kits/RoktKit.kt index 7c3752a..26c78a2 100644 --- a/src/main/kotlin/com/mparticle/kits/RoktKit.kt +++ b/src/main/kotlin/com/mparticle/kits/RoktKit.kt @@ -21,7 +21,6 @@ import com.mparticle.kits.KitIntegration.IdentityListener import com.mparticle.kits.KitIntegration.RoktListener import com.mparticle.rokt.RoktConfig import com.mparticle.rokt.RoktEmbeddedView -import com.rokt.roktsdk.CacheConfig import com.rokt.roktsdk.Rokt import com.rokt.roktsdk.Rokt.RoktCallback import com.rokt.roktsdk.Rokt.SdkFrameworkType.Android @@ -197,7 +196,7 @@ class RoktKit : this.mpRoktEventCallback = mpRoktEventCallback val finalAttributes = prepareFinalAttributes(filterUser, attributes) - val roktConfig = mpRoktConfig?.let { mapToRoktConfig(it) } + val roktConfig = mpRoktConfig?.toRoktSdkConfig() Rokt.execute( viewName, finalAttributes, @@ -214,25 +213,48 @@ class RoktKit : attributes: Map, ): Map { val finalAttributes = mutableMapOf() + filterUser?.userAttributes?.let { userAttrs -> for ((key, value) in userAttrs) { - finalAttributes[key] = value.toString() + if (value != null) { + finalAttributes[key] = convertValueToString(value) + } } } + finalAttributes.putAll(filterAttributes(attributes, configuration)) + filterUser?.id?.toString()?.let { mpid -> finalAttributes[MPID] = mpid } ?: Logger.warning("RoktKit: No user ID available for placement") addIdentityAttributes(finalAttributes, filterUser) - attributes[ROKT_ATTRIBUTE_SANDBOX_MODE]?.let { value -> - finalAttributes.put(ROKT_ATTRIBUTE_SANDBOX_MODE, value) - } verifyHashedEmail(finalAttributes) return finalAttributes } + private fun convertValueToString(value: Any?): String { + return when (value) { + is Double -> BigDecimal.valueOf(value).toPlainString() + is Long -> BigDecimal.valueOf(value).toPlainString() + is Int -> BigDecimal.valueOf(value.toLong()).toPlainString() + is Number -> BigDecimal(value.toString()).toPlainString() + else -> value.toString() + } + } + + private fun filterAttributes(attributes: Map, kitConfiguration: KitConfiguration): MutableMap { + val userAttributes = mutableMapOf() + for ((key, value) in attributes) { + val hashKey = KitUtils.hashForFiltering(key) + if (!kitConfiguration.mUserAttributeFilters.get(hashKey)) { + userAttributes[key] = value + } + } + return userAttributes + } + override fun events(identifier: String): Flow = Rokt.events(identifier).map { event -> when (event) { is RoktEvent.HideLoadingIndicator -> com.mparticle.RoktEvent.HideLoadingIndicator @@ -312,30 +334,7 @@ class RoktKit : } } - private fun mapToRoktConfig(config: RoktConfig): com.rokt.roktsdk.RoktConfig { - val colorMode = when (config.colorMode) { - RoktConfig.ColorMode.LIGHT -> com.rokt.roktsdk.RoktConfig.ColorMode.LIGHT - RoktConfig.ColorMode.DARK -> com.rokt.roktsdk.RoktConfig.ColorMode.DARK - RoktConfig.ColorMode.SYSTEM -> com.rokt.roktsdk.RoktConfig.ColorMode.SYSTEM - else -> com.rokt.roktsdk.RoktConfig.ColorMode.SYSTEM - } - - val cacheConfig = config.cacheConfig?.cacheDurationInSeconds?.let { - CacheConfig( - cacheDurationInSeconds = it, - cacheAttributes = config.cacheConfig?.cacheAttributes, - ) - } - val edgeToEdgeDisplay = config.edgeToEdgeDisplay - - val builder = com.rokt.roktsdk.RoktConfig.Builder().colorMode(colorMode).edgeToEdgeDisplay(edgeToEdgeDisplay) - - cacheConfig?.let { - builder.cacheConfig(it) - } - return builder.build() - } private fun addIdentityAttributes( attributes: MutableMap?, diff --git a/src/main/kotlin/com/mparticle/kits/RoktLayout.kt b/src/main/kotlin/com/mparticle/kits/RoktLayout.kt index 610c1aa..c18fd53 100644 --- a/src/main/kotlin/com/mparticle/kits/RoktLayout.kt +++ b/src/main/kotlin/com/mparticle/kits/RoktLayout.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.mparticle.MpRoktEventCallback +import com.mparticle.rokt.RoktConfig import com.rokt.roktsdk.Rokt @Composable @@ -17,6 +18,7 @@ fun RoktLayout( location: String, modifier: Modifier = Modifier, mpRoktEventCallback: MpRoktEventCallback? = null, + config: RoktConfig? = null, ) { val instance = RoktKit.instance val resultMapState = remember { mutableStateOf(null) } @@ -38,6 +40,7 @@ fun RoktLayout( onShouldShowLoadingIndicator = { resultMap.callback.onShouldShowLoadingIndicator() }, onShouldHideLoadingIndicator = { resultMap.callback.onShouldHideLoadingIndicator() }, onUnload = { reason -> resultMap.callback.onUnload(reason) }, + config = config?.toRoktSdkConfig(), ) } } diff --git a/src/test/kotlin/com/mparticle/kits/RoktConfigExtensionsTest.kt b/src/test/kotlin/com/mparticle/kits/RoktConfigExtensionsTest.kt new file mode 100644 index 0000000..0d95c4d --- /dev/null +++ b/src/test/kotlin/com/mparticle/kits/RoktConfigExtensionsTest.kt @@ -0,0 +1,67 @@ +package com.mparticle.kits + +import com.mparticle.rokt.CacheConfig +import com.mparticle.rokt.RoktConfig +import com.rokt.roktsdk.RoktConfig as SdkRoktConfig +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.Assert.assertNotNull + +class RoktConfigExtensionsTest { + + @Test + fun `toRoktSdkConfig maps color mode and edgeToEdge`() { + val source = mockk() + every { source.colorMode } returns RoktConfig.ColorMode.DARK + every { source.edgeToEdgeDisplay } returns true + every { source.cacheConfig } returns null + + val result: SdkRoktConfig = source.toRoktSdkConfig() + + assertEquals(SdkRoktConfig.ColorMode.DARK, result.colorMode) + assertEquals(true, result.edgeToEdgeDisplay) + } + + @Test + fun `toRoktSdkConfig maps cacheConfig when present`() { + val cacheAttributes = mapOf( + "key1" to "value1", + "key2" to "value2", + ) + + val cacheConfig = mockk() + every { cacheConfig.cacheDurationInSeconds } returns 3600 + every { cacheConfig.cacheAttributes } returns cacheAttributes + + val source = mockk() + every { source.colorMode } returns RoktConfig.ColorMode.LIGHT + every { source.edgeToEdgeDisplay } returns false + every { source.cacheConfig } returns cacheConfig + + val result: SdkRoktConfig = source.toRoktSdkConfig() + + assertEquals(SdkRoktConfig.ColorMode.LIGHT, result.colorMode) + assertEquals(false, result.edgeToEdgeDisplay) + assertNotNull(result.cacheConfig) + assertEquals(3600L, result.cacheConfig?.cacheDurationInSeconds) + assertEquals(cacheAttributes, result.cacheConfig?.cacheAttributes) + } + + @Test + fun `toRoktSdkCacheConfig maps fields`() { + val cacheAttributes = mapOf( + "a" to "1", + "b" to "2", + ) + val mpCache = mockk() + every { mpCache.cacheDurationInSeconds } returns 120 + every { mpCache.cacheAttributes } returns cacheAttributes + + val sdkCache = mpCache.toRoktSdkCacheConfig() + + assertEquals(120, sdkCache.cacheDurationInSeconds) + assertEquals(cacheAttributes, sdkCache.cacheAttributes) + } +} diff --git a/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt b/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt index 3783d70..5dd583e 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.kits.mocks.MockKitConfiguration import com.rokt.roktsdk.FulfillmentAttributes import com.rokt.roktsdk.Rokt import com.rokt.roktsdk.RoktEvent @@ -23,6 +24,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.runs +import io.mockk.slot import io.mockk.unmockkObject import io.mockk.verify import io.mockk.verifyOrder @@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.json.JSONArray +import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -74,14 +77,339 @@ class RoktKitTests { // roktKit.configuration = KitConfiguration.createKitConfiguration(JSONObject().put("id", "-1")) } + @Test + fun test_prepareFinalAttributes_filters_out_null_user_attributes() { + val mockFilterUser = mock(FilteredMParticleUser::class.java) + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap()) + + // Include a null value and a non-null non-string value to verify toString() behavior + val userAttributes = HashMap() + userAttributes["attr_non_null_string"] = "value" + userAttributes["attr_null"] = null + userAttributes["attr_non_string"] = 123 + Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes) + // Set up the configuration with our test filters + val jsonObject = JSONObject() + try { + val filteredKey: String = KitUtils.hashForFiltering("ShouldFilter").toString() + val filteredKey2: String = KitUtils.hashForFiltering("ShouldFilter_key_2").toString() + jsonObject.put(filteredKey, 0) + jsonObject.put(filteredKey2, 1) + } catch (e: Exception) { + println("Exception occurred: ${e.message}") + } + val json = JSONObject() + json.put("ua", jsonObject) + + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", json)) + val method: Method = RoktKit::class.java.getDeclaredMethod( + "prepareFinalAttributes", + FilteredMParticleUser::class.java, + Map::class.java, + ) + method.isAccessible = true + + val inputAttributes: Map = emptyMap() + val result = method.invoke(roktKit, mockFilterUser, inputAttributes) as Map<*, *> + + // Should include only non-null user attributes, and convert non-string values via toString() + assertTrue(result.containsKey("attr_non_null_string")) + assertEquals("value", result["attr_non_null_string"]) + + assertFalse(result.containsKey("attr_null")) + + assertTrue(result.containsKey("attr_non_string")) + assertEquals("123", result["attr_non_string"]) + } + + @Test + fun test_prepareFinalAttributes_converts_large_double_without_scientific_notation() { + // Arrange + mockkObject(Rokt) + val capturedAttributesSlot = slot>() + every { + Rokt.execute( + any(), + capture(capturedAttributesSlot), + any(), + null, + null, + null, + ) + } just runs + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap()) + Mockito.`when`(mockFilterUser.id).thenReturn(12345L) + + val userAttributes = HashMap() + // Large Double value that would produce scientific notation with toString() + userAttributes["large_double"] = 50352212112.0 + userAttributes["large_double_negative"] = -50352212112.0 + userAttributes["double_with_decimal"] = 123.456789 + userAttributes["double_zero"] = 0.0 + Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject())) + + // Act + val inputAttributes: Map = mapOf("initial_attr" to "initial_value") + roktKit.execute( + viewName = "test_view", + attributes = inputAttributes, + mpRoktEventCallback = null, + placeHolders = null, + fontTypefaces = null, + filterUser = mockFilterUser, + mpRoktConfig = null, + ) + + // Assert + val capturedAttributes = capturedAttributesSlot.captured + assertEquals("50352212112", capturedAttributes["large_double"]) + + assertEquals("-50352212112", capturedAttributes["large_double_negative"]) + + assertTrue(capturedAttributes.containsKey("double_with_decimal")) + assertEquals("123.456789", capturedAttributes["double_with_decimal"]) + + assertEquals("0.0", capturedAttributes["double_zero"]) + + // Verify initial attributes are preserved + assertEquals("initial_value", capturedAttributes["initial_attr"]) + + unmockkObject(Rokt) + } + + @Test + fun test_prepareFinalAttributes_converts_long_values() { + // Arrange + mockkObject(Rokt) + val capturedAttributesSlot = slot>() + every { + Rokt.execute( + any(), + capture(capturedAttributesSlot), + any(), + null, + null, + null, + ) + } just runs + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap()) + Mockito.`when`(mockFilterUser.id).thenReturn(12345L) + + val userAttributes = HashMap() + userAttributes["long_value"] = Long.MAX_VALUE // 9223372036854775807L + userAttributes["long_negative"] = Long.MIN_VALUE // -9223372036854775808L + userAttributes["long_zero"] = 0L + Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject())) + + // Act + roktKit.execute( + viewName = "test_view", + attributes = emptyMap(), + mpRoktEventCallback = null, + placeHolders = null, + fontTypefaces = null, + filterUser = mockFilterUser, + mpRoktConfig = null, + ) + + // Assert + val capturedAttributes = capturedAttributesSlot.captured + assertEquals("9223372036854775807", capturedAttributes["long_value"]) + assertEquals("-9223372036854775808", capturedAttributes["long_negative"]) + assertEquals("0", capturedAttributes["long_zero"]) + + unmockkObject(Rokt) + } + + @Test + fun test_prepareFinalAttributes_converts_int_values() { + // Arrange + mockkObject(Rokt) + val capturedAttributesSlot = slot>() + every { + Rokt.execute( + any(), + capture(capturedAttributesSlot), + any(), + null, + null, + null, + ) + } just runs + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap()) + Mockito.`when`(mockFilterUser.id).thenReturn(12345L) + + val userAttributes = HashMap() + userAttributes["int_value"] = Int.MAX_VALUE // 2147483647 + userAttributes["int_negative"] = Int.MIN_VALUE // -2147483648 + userAttributes["int_zero"] = 0 + Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject())) + + // Act + roktKit.execute( + viewName = "test_view", + attributes = emptyMap(), + mpRoktEventCallback = null, + placeHolders = null, + fontTypefaces = null, + filterUser = mockFilterUser, + mpRoktConfig = null, + ) + + // Assert + val capturedAttributes = capturedAttributesSlot.captured + assertEquals("2147483647", capturedAttributes["int_value"]) + assertEquals("-2147483648", capturedAttributes["int_negative"]) + assertEquals("0", capturedAttributes["int_zero"]) + + unmockkObject(Rokt) + } + + @Test + fun test_prepareFinalAttributes_preserves_string_values() { + // Arrange + mockkObject(Rokt) + val capturedAttributesSlot = slot>() + every { + Rokt.execute( + any(), + capture(capturedAttributesSlot), + any(), + null, + null, + null, + ) + } just runs + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap()) + Mockito.`when`(mockFilterUser.id).thenReturn(12345L) + + val userAttributes = HashMap() + userAttributes["string_value"] = "test_string" + userAttributes["string_with_numbers"] = "123abc456" + userAttributes["empty_string"] = "" + Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject())) + + // Act + roktKit.execute( + viewName = "test_view", + attributes = emptyMap(), + mpRoktEventCallback = null, + placeHolders = null, + fontTypefaces = null, + filterUser = mockFilterUser, + mpRoktConfig = null, + ) + + // Assert + val capturedAttributes = capturedAttributesSlot.captured + assertEquals("test_string", capturedAttributes["string_value"]) + assertEquals("123abc456", capturedAttributes["string_with_numbers"]) + assertEquals("", capturedAttributes["empty_string"]) + + unmockkObject(Rokt) + } + + + @Test + fun test_prepareFinalAttributes_handlesSameKeysInAttributesAndUserAttributes() { + // Arrange + mockkObject(Rokt) + val capturedAttributesSlot = slot>() + every { + Rokt.execute( + any(), + capture(capturedAttributesSlot), + any(), + null, + null, + null, + ) + } just runs + + val mockFilterUser = mock(FilteredMParticleUser::class.java) + Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap()) + Mockito.`when`(mockFilterUser.id).thenReturn(12345L) + + val userAttributes = HashMap() + userAttributes["attr_non_null_string"] = "value" + userAttributes["attr_null"] = null + userAttributes["attr_non_string"] = 123 + userAttributes["user_key"] = "1231545" + Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes) + // Set up the configuration with our test filters + val jsonObject = JSONObject() + try { + val filteredKey: String = KitUtils.hashForFiltering("ShouldFilter").toString() + val filteredKey2: String = KitUtils.hashForFiltering("ShouldFilter_key_2").toString() + jsonObject.put(filteredKey, 0) + jsonObject.put(filteredKey2, 1) + } catch (e: Exception) { + println("Exception occurred: ${e.message}") + } + val json = JSONObject() + json.put("ua", jsonObject) + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", json)) + val inputAttributes: Map = mapOf( + "key1" to "value1", + "key2" to "value2", + "key3" to "value3", + "user_key" to "2223333", + "ShouldFilter" to "testData" + ) + // Act + roktKit.execute( + viewName = "test", + attributes = inputAttributes, + mpRoktEventCallback = null, + placeHolders = null, + fontTypefaces = null, + filterUser = mockFilterUser, + mpRoktConfig = null, + ) + + // Assert + val capturedAttributes = capturedAttributesSlot.captured + + + assertEquals(7, capturedAttributes.size) + assertEquals("value", capturedAttributes["attr_non_null_string"]) + assertEquals("123", capturedAttributes["attr_non_string"]) + assertEquals("2223333", capturedAttributes["user_key"]) + assertEquals("value1", capturedAttributes["key1"]) + assertEquals("value2", capturedAttributes["key2"]) + assertEquals("value3", capturedAttributes["key3"]) + assertEquals("12345", capturedAttributes["mpid"]) + + assertFalse(capturedAttributes.containsKey("ShouldFilter")) + assertFalse(capturedAttributes.containsKey("ShouldFilter_key_2")) + unmockkObject(Rokt) + } + private inner class TestKitManager : KitManagerImpl(context, null, TestCoreCallbacks(), mock(MParticleOptions::class.java)) { var attributes = HashMap() var result: AttributionResult? = null private var error: AttributionError? = null - public override fun getIntegrationAttributes(kitIntegration: KitIntegration): Map = attributes + override fun getIntegrationAttributes(kitIntegration: KitIntegration): Map = attributes - public override fun setIntegrationAttributes( + override fun setIntegrationAttributes( kitIntegration: KitIntegration, integrationAttributes: Map, ) { @@ -661,6 +989,165 @@ class RoktKitTests { assertFalse(attributes.containsKey("emailsha256")) } + @Test + fun testFilterAttributes() { + // Create test attributes + val attributes: Map = mapOf( + "ShouldFilter" to "shoudl_filter_value", + "ShouldFilter_key_2" to "ShouldFilter_value", + "allowed_key" to "allowed_value", + ) + + // Get the private filterAttributes method using reflection + val method: Method = RoktKit::class.java.getDeclaredMethod( + "filterAttributes", + Map::class.java, + KitConfiguration::class.java, + ) + method.isAccessible = true + + // Set up the configuration with our test filters + val jsonObject = JSONObject() + try { + val filteredKey: String = KitUtils.hashForFiltering("ShouldFilter").toString() + val filteredKey2: String = KitUtils.hashForFiltering("ShouldFilter_key_2").toString() + jsonObject.put(filteredKey, 0) + jsonObject.put(filteredKey2, 1) + } catch (e: Exception) { + println("Exception occurred: ${e.message}") + } + + val json = JSONObject() + json.put("ua", jsonObject) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", json)) + + // Invoke the method and get the result + val result = method.invoke(roktKit, attributes, roktKit.configuration) as Map + + // Verify the results + assertEquals(1, result.size) + + assertFalse(result.containsKey("ShouldFilter")) + assertFalse(result.containsKey("ShouldFilter_key_2")) + assertTrue(result.containsKey("allowed_key")) + assertEquals("allowed_value", result["allowed_key"]) + } + + @Test + fun testFilterAttributes_When_kitConfig_User_Attributes_Filtering_IS_NULL() { + // Create test attributes + val attributes: Map = mapOf( + "filtered_key" to "filtered_value", + "allowed_key" to "allowed_value", + "another_allowed_key" to "another_allowed_value", + ) + + // Get the private filterAttributes method using reflection + val method: Method = RoktKit::class.java.getDeclaredMethod( + "filterAttributes", + Map::class.java, + KitConfiguration::class.java, + ) + method.isAccessible = true + + val json = JSONObject() + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", json)) + + // Invoke the method and get the result + val result = method.invoke(roktKit, attributes, roktKit.configuration) as Map + + assertEquals(3, result.size) + + assertTrue(result.containsKey("allowed_key")) + assertTrue(result.containsKey("filtered_key")) + assertTrue(result.containsKey("another_allowed_key")) + assertEquals("another_allowed_value", result["another_allowed_key"]) + } + + @Test + fun testFilterAttributes_When_Attributes_IS_Empty() { + // Create test attributes + val emptyAttributes: Map = emptyMap() + + // Get the private filterAttributes method using reflection + val method: Method = RoktKit::class.java.getDeclaredMethod( + "filterAttributes", + Map::class.java, + KitConfiguration::class.java, + ) + method.isAccessible = true + + // Set up the configuration with our test filters + val jsonObject = JSONObject() + try { + val filteredKey: String = KitUtils.hashForFiltering("filtered_key").toString() + val filteredKey2: String = KitUtils.hashForFiltering("allowed_key").toString() + jsonObject.put(filteredKey, 0) + jsonObject.put(filteredKey2, 1) + } catch (e: Exception) { + println("Exception occurred: ${e.message}") + } + + val json = JSONObject() + json.put("aaa", jsonObject) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", json)) + + // Invoke the method and get the result + val result = method.invoke(roktKit, emptyAttributes, roktKit.configuration) as Map + + assertEquals(0, result.size) + } + + @Test + fun testFilterAttributes_Allows_Attributes_To_Be_Set_When_When_Filtered_User_Attributes_Differ() { + // Create test attributes + val attributes: Map = mapOf( + "filtered_key" to "filtered_value", + "allowed_key" to "allowed_value", + "another_allowed_key" to "another_allowed_value", + ) + + // Get the private filterAttributes method using reflection + val method: Method = RoktKit::class.java.getDeclaredMethod( + "filterAttributes", + Map::class.java, + KitConfiguration::class.java, + ) + method.isAccessible = true + + // Set up the configuration with our test filters + val jsonObject = JSONObject() + try { + val filteredKey: String = KitUtils.hashForFiltering("Test1").toString() + val filteredKey2: String = KitUtils.hashForFiltering("Test2").toString() + jsonObject.put(filteredKey, 0) + jsonObject.put(filteredKey2, 1) + } catch (e: Exception) { + println("Exception occurred: ${e.message}") + } + + val json = JSONObject() + json.put("us", jsonObject) + + roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", json)) + + // Invoke the method and get the result + val result = method.invoke(roktKit, attributes, roktKit.configuration) as Map + + // Verify the results + assertEquals(3, result.size) + + assertTrue(result.containsKey("filtered_key")) + assertTrue(result.containsKey("allowed_key")) + assertTrue(result.containsKey("another_allowed_key")) + assertEquals("another_allowed_value", result["another_allowed_key"]) + assertEquals("filtered_value", result["filtered_key"]) + assertEquals("allowed_value", result["allowed_key"]) + } + internal inner class TestCoreCallbacks : CoreCallbacks { override fun isBackgrounded(): Boolean = false override fun getUserBucket(): Int = 0