diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/abtests/InAppABTestLogic.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/abtests/InAppABTestLogic.kt new file mode 100644 index 000000000..2d3019a46 --- /dev/null +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/abtests/InAppABTestLogic.kt @@ -0,0 +1,70 @@ +package cloud.mindbox.mobile_sdk.abtests + +import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.MobileConfigRepository +import cloud.mindbox.mobile_sdk.inapp.domain.models.ABTest +import cloud.mindbox.mobile_sdk.logger.MindboxLog +import cloud.mindbox.mobile_sdk.repository.MindboxPreferences + +internal class InAppABTestLogic( + private val mixer: CustomerAbMixer, + private val repository: MobileConfigRepository, +): MindboxLog { + + suspend fun getInAppsPool(allInApps: List): Set { + val abtests = repository.getABTests() + val uuid = MindboxPreferences.deviceUuid + + if (abtests.isEmpty()) { + logI("Abtests is empty. Use all inApps.") + return allInApps.toSet() + } + if (allInApps.isEmpty()) { + return emptySet() + } + + val inAppsForAbtest: List> = abtests.map { abtest -> + + val hash = mixer.stringModulusHash( + identifier = uuid, + salt = abtest.salt.uppercase(), + ) + + logI("Mixer calculate hash $hash for abtest ${abtest.id} with salt ${abtest.salt} and deviceUuid $uuid") + + val targetVariantInApps: MutableSet = mutableSetOf() + val otherVariantInApps: MutableSet = mutableSetOf() + + abtest.variants.onEach { variant -> + val inApps: List = when (variant.kind) { + ABTest.Variant.VariantKind.ALL -> allInApps + ABTest.Variant.VariantKind.CONCRETE -> variant.inapps + } + if ((variant.lower until variant.upper).contains(hash)) { + targetVariantInApps.addAll(inApps) + logI("Selected variant $variant for ${abtest.id}") + } else { + otherVariantInApps.addAll(inApps) + } + } + + (targetVariantInApps + (allInApps - otherVariantInApps)).also { inappsSet -> + logI("For abtest ${abtest.id} determined $inappsSet") + } + } + + if (inAppsForAbtest.isEmpty()) { + logW("No inApps after calculation abtests logic. InApp will not be shown.") + return setOf() + } + + return if (inAppsForAbtest.size == 1) { + inAppsForAbtest.first() + } else { + getIntersectionForAllABTests(inAppsForAbtest) + } + } + + private fun getIntersectionForAllABTests(inAppsForAbtest: List>): Set = + inAppsForAbtest.reduce { acc, list -> acc.intersect(list) } + +} \ No newline at end of file diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt index 614e1e336..2fe99b929 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt @@ -1,7 +1,9 @@ package cloud.mindbox.mobile_sdk.di.modules + import cloud.mindbox.mobile_sdk.abtests.CustomerAbMixer import cloud.mindbox.mobile_sdk.abtests.CustomerAbMixerImpl +import cloud.mindbox.mobile_sdk.abtests.InAppABTestLogic import cloud.mindbox.mobile_sdk.inapp.domain.InAppChoosingManagerImpl import cloud.mindbox.mobile_sdk.inapp.domain.InAppEventManagerImpl import cloud.mindbox.mobile_sdk.inapp.domain.InAppFilteringManagerImpl @@ -26,7 +28,8 @@ internal fun DomainModule( inAppSegmentationRepository = inAppSegmentationRepository, inAppFilteringManager = inAppFilteringManager, inAppEventManager = inAppEventManager, - inAppChoosingManager = inAppChoosingManager + inAppChoosingManager = inAppChoosingManager, + inAppABTestLogic = inAppABTestLogic, ) } @@ -44,6 +47,12 @@ internal fun DomainModule( override val inAppFilteringManager: InAppFilteringManager get() = InAppFilteringManagerImpl(inAppRepository = inAppRepository) + override val inAppABTestLogic: InAppABTestLogic + get() = InAppABTestLogic( + mixer = customerAbMixer, + repository = mobileConfigRepository + ) + override val customerAbMixer: CustomerAbMixer get() = CustomerAbMixerImpl() } \ No newline at end of file diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt index 7d3fbacc2..532ca6dd4 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt @@ -2,6 +2,7 @@ package cloud.mindbox.mobile_sdk.di.modules import android.app.Application import cloud.mindbox.mobile_sdk.abtests.CustomerAbMixer +import cloud.mindbox.mobile_sdk.abtests.InAppABTestLogic import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager import cloud.mindbox.mobile_sdk.inapp.data.mapper.InAppMapper import cloud.mindbox.mobile_sdk.inapp.data.validators.ABTestValidator @@ -87,6 +88,7 @@ internal interface DomainModule : MindboxModule { val inAppEventManager: InAppEventManager val inAppFilteringManager: InAppFilteringManager val customerAbMixer: CustomerAbMixer + val inAppABTestLogic: InAppABTestLogic } internal interface ApiModule : MindboxModule { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt index f1bb9f069..579874aa7 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt @@ -115,6 +115,7 @@ internal class InAppMapper { salt = dto.salt!!, variants = dto.variants?.map { variantDto -> ABTest.Variant( + id = variantDto.id, type = variantDto.objects!!.first().type!!, kind = variantDto.objects.first().kind.enumValue(), inapps = variantDto.objects.first().inapps ?: listOf(), diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidator.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidator.kt index ec672504c..c16e2a04d 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidator.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidator.kt @@ -12,6 +12,11 @@ internal class VariantValidator : Validator { return false } + if (item.id.isBlank()) { + mindboxLogW("The 'id' field can not be null or empty") + return false + } + if (item.modulus == null) { mindboxLogW("The 'modulus' field can not be null") return false @@ -32,7 +37,6 @@ internal class VariantValidator : Validator { return false } - if (item.objects.size != 1) { mindboxLogW("The 'objects' field must be only one") return false diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt index bb15eafac..532627a62 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt @@ -1,6 +1,7 @@ package cloud.mindbox.mobile_sdk.inapp.domain import cloud.mindbox.mobile_sdk.InitializeLock +import cloud.mindbox.mobile_sdk.abtests.InAppABTestLogic import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.interactors.InAppInteractor import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppChoosingManager import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppEventManager @@ -11,7 +12,7 @@ import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.MobileConfi import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType import cloud.mindbox.mobile_sdk.inapp.domain.models.ProductSegmentationFetchStatus -import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl +import cloud.mindbox.mobile_sdk.logger.MindboxLog import cloud.mindbox.mobile_sdk.logger.mindboxLogD import cloud.mindbox.mobile_sdk.models.InAppEventType import kotlinx.coroutines.flow.* @@ -22,26 +23,31 @@ internal class InAppInteractorImpl( private val inAppSegmentationRepository: InAppSegmentationRepository, private val inAppFilteringManager: InAppFilteringManager, private val inAppEventManager: InAppEventManager, - private val inAppChoosingManager: InAppChoosingManager -) : InAppInteractor { + private val inAppChoosingManager: InAppChoosingManager, + private val inAppABTestLogic: InAppABTestLogic, +) : InAppInteractor, MindboxLog { override suspend fun processEventAndConfig(): Flow { - val inApps: List = mobileConfigRepository.getInAppsSection().let { inApps -> - inAppFilteringManager.filterNotShownInApps( - inAppRepository.getShownInApps(), - inApps - ) - }.also { unShownInApps -> - MindboxLoggerImpl.d( - this, "Filtered config has ${unShownInApps.size} inapps" - ) - inAppSegmentationRepository.unShownInApps = unShownInApps - for (inApp in unShownInApps) { - for (operation in inApp.targeting.getOperationsSet()) { - inAppRepository.saveOperationalInApp(operation.lowercase(), inApp) + val inApps: List = mobileConfigRepository.getInAppsSection() + .let { inApps -> + val inAppIds = inAppABTestLogic.getInAppsPool(inApps.map { it.id }) + inAppFilteringManager.filterABTestsInApps(inApps, inAppIds).also { filteredInApps -> + logI("InApps after abtest logic ${filteredInApps.map { it.id }}") + } + }.let { inApps -> + inAppFilteringManager.filterNotShownInApps( + inAppRepository.getShownInApps(), + inApps + ) + }.also { unShownInApps -> + logI("Filtered config has ${unShownInApps.size} inapps") + inAppSegmentationRepository.unShownInApps = unShownInApps + for (inApp in unShownInApps) { + for (operation in inApp.targeting.getOperationsSet()) { + inAppRepository.saveOperationalInApp(operation.lowercase(), inApp) + } } } - } return inAppRepository.listenInAppEvents() .filter { event -> inAppEventManager.isValidInAppEvent(event) } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt index 566d24d97..4504e7839 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt @@ -58,6 +58,7 @@ internal data class ABTest( val variants: List, ) { internal data class Variant( + val id: String, val type: String, val kind: VariantKind, val lower: Int, diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/logger/MindboxLoggerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/logger/MindboxLoggerImpl.kt index c35e552ea..ae1c037c9 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/logger/MindboxLoggerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/logger/MindboxLoggerImpl.kt @@ -129,3 +129,9 @@ internal fun Any.mindboxLogE(message: String, exception: Throwable? = null) = ex MindboxLoggerImpl.e(this, message, exception) } ?: MindboxLoggerImpl.e(this, message) +internal interface MindboxLog { + fun logD(message: String) = this.mindboxLogD(message) + fun logI(message: String) = this.mindboxLogI(message) + fun logW(message: String, exception: Throwable? = null) = this.mindboxLogW(message, exception) + fun logE(message: String, exception: Throwable? = null) = this.mindboxLogE(message, exception) +} diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt index 04f2f05eb..848f4e10f 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt @@ -37,6 +37,8 @@ internal data class ABTestDto( val variants: List?, ) { internal data class VariantDto( + @SerializedName("id") + val id: String, @SerializedName("modulus") val modulus: ModulusDto?, @SerializedName("objects") diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/abtests/InAppABTestLogicTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/abtests/InAppABTestLogicTest.kt new file mode 100644 index 000000000..bff834b6e --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/abtests/InAppABTestLogicTest.kt @@ -0,0 +1,448 @@ +package cloud.mindbox.mobile_sdk.abtests + +import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.MobileConfigRepository +import cloud.mindbox.mobile_sdk.inapp.domain.models.ABTest +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class InAppABTestLogicTest { + + private val threeInapps = listOf( + "655f5ffa-de86-4224-a0bf-229fe208ed0d", + "6f93e2ef-0615-4e63-9c80-24bcb9e83b83", + "b33ca779-3c99-481f-ad46-91282b0caf04", + ) + private val fourInapps = listOf( + "655f5ffa-de86-4224-a0bf-229fe208ed0d", + "6f93e2ef-0615-4e63-9c80-24bcb9e83b83", + "b33ca779-3c99-481f-ad46-91282b0caf04", + "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", + ) + + private val abtest = ABTest("", null, null, "", listOf()) + private val variant = ABTest.Variant("", "inapps", ABTest.Variant.VariantKind.ALL, 0, 100, listOf()) + + @Test + fun `abtest logic is empty variants`() = runTest { + assertEquals(threeInapps.toSet(), calculateInApps(25, listOf(), threeInapps)) + } + + @Test + fun `abtest logic two variants with config of three inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 50, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf() + ), + variant.copy( + lower = 50, + upper = 100, + kind = ABTest.Variant.VariantKind.ALL, + inapps = listOf() + ) + ) + ) + ) + + assertEquals(setOf(), calculateInApps(25, abtests, threeInapps)) + assertEquals(threeInapps.toSet(), calculateInApps(75, abtests, threeInapps)) + + val inapps1withExtra = threeInapps + "test" + assertEquals(setOf(), calculateInApps(25, abtests, inapps1withExtra)) + assertEquals(threeInapps.toSet() + "test", calculateInApps(75, abtests, inapps1withExtra)) + } + + @Test + fun `abtest logic two variants with config of four inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 50, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf() + ), + variant.copy( + lower = 50, + upper = 100, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf( + "655f5ffa-de86-4224-a0bf-229fe208ed0d", + "b33ca779-3c99-481f-ad46-91282b0caf04" + ) + ) + ) + ) + ) + + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b"), + calculateInApps(0, abtests, fourInapps) + ) + assertEquals( + fourInapps.toSet(), + calculateInApps(99, abtests, fourInapps) + ) + + val inapps2withExtra = fourInapps + "test" + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", "test"), + calculateInApps(0, abtests, inapps2withExtra) + ) + assertEquals( + fourInapps.toSet() + "test", + calculateInApps(99, abtests, inapps2withExtra) + ) + } + + @Test + fun `abtest logic three variants config of three inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 30, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf() + ), + variant.copy( + lower = 30, + upper = 65, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf( + "655f5ffa-de86-4224-a0bf-229fe208ed0d", + "b33ca779-3c99-481f-ad46-91282b0caf04" + ) + ), + variant.copy( + lower = 65, + upper = 100, + kind = ABTest.Variant.VariantKind.ALL, + inapps = listOf() + ) + ) + ) + ) + + assertEquals( + emptySet(), + calculateInApps(1, abtests, threeInapps) + ) + assertEquals( + setOf("655f5ffa-de86-4224-a0bf-229fe208ed0d", "b33ca779-3c99-481f-ad46-91282b0caf04"), + calculateInApps(30, abtests, threeInapps) + ) + assertEquals( + threeInapps.toSet(), + calculateInApps(65, abtests, threeInapps) + ) + } + + @Test + fun `abtest logic three variants with config of four inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 27, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf() + ), + variant.copy( + lower = 27, + upper = 65, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("655f5ffa-de86-4224-a0bf-229fe208ed0d") + ), + variant.copy( + lower = 65, + upper = 100, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("b33ca779-3c99-481f-ad46-91282b0caf04") + ) + ) + ) + ) + + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b"), + calculateInApps(10, abtests, fourInapps) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", "655f5ffa-de86-4224-a0bf-229fe208ed0d"), + calculateInApps(64, abtests, fourInapps) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", "b33ca779-3c99-481f-ad46-91282b0caf04"), + calculateInApps(65, abtests, fourInapps) + ) + + val inapps2withExtra = fourInapps + "!" + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", "!"), + calculateInApps(10, abtests, inapps2withExtra) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", "655f5ffa-de86-4224-a0bf-229fe208ed0d", "!"), + calculateInApps(64, abtests, inapps2withExtra) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "d1b312bd-aa5c-414c-a0d8-8126376a2a9b", "b33ca779-3c99-481f-ad46-91282b0caf04", "!"), + calculateInApps(65, abtests, inapps2withExtra) + ) + } + + @Test + fun `abtest logic two concrete with config of three inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 99, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf( + "655f5ffa-de86-4224-a0bf-229fe208ed0d", + "b33ca779-3c99-481f-ad46-91282b0caf04" + ) + ), + variant.copy( + lower = 99, + upper = 100, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83") + ) + ) + ) + ) + + assertEquals( + setOf("655f5ffa-de86-4224-a0bf-229fe208ed0d", "b33ca779-3c99-481f-ad46-91282b0caf04"), + calculateInApps(98, abtests, threeInapps) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83"), + calculateInApps(99, abtests, threeInapps) + ) + + val inapps1withExtra = threeInapps + "??" + assertEquals( + setOf("655f5ffa-de86-4224-a0bf-229fe208ed0d", "b33ca779-3c99-481f-ad46-91282b0caf04", "??"), + calculateInApps(98, abtests, inapps1withExtra) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83", "??"), + calculateInApps(99, abtests, inapps1withExtra) + ) + } + + @Test + fun `abtest logic two concrete variants without inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 99, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf( + "655f5ffa-de86-4224-a0bf-229fe208ed0d", + "b33ca779-3c99-481f-ad46-91282b0caf04" + ) + ), + variant.copy( + lower = 99, + upper = 100, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83") + ) + ) + ) + ) + + assertEquals( + emptySet(), + calculateInApps(0, abtests, listOf()) + ) + assertEquals( + emptySet(), + calculateInApps(99, abtests, listOf()) + ) + } + + @Test + fun `abtest logic five variants with config of four inapps`() = runTest { + val abtests = listOf( + abtest.copy( + variants = listOf( + variant.copy( + lower = 0, + upper = 10, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("655f5ffa-de86-4224-a0bf-229fe208ed0d") + ), + variant.copy( + lower = 10, + upper = 20, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83") + ), + variant.copy( + lower = 20, + upper = 30, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("b33ca779-3c99-481f-ad46-91282b0caf04") + ), + variant.copy( + lower = 30, + upper = 70, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("d1b312bd-aa5c-414c-a0d8-8126376a2a9b") + ), + variant.copy( + lower = 70, + upper = 100, + kind = ABTest.Variant.VariantKind.ALL, + inapps = listOf() + ) + ) + ) + ) + + assertEquals( + setOf("655f5ffa-de86-4224-a0bf-229fe208ed0d"), + calculateInApps(5, abtests, fourInapps) + ) + assertEquals( + setOf("6f93e2ef-0615-4e63-9c80-24bcb9e83b83"), + calculateInApps(15, abtests, fourInapps) + ) + assertEquals( + setOf("b33ca779-3c99-481f-ad46-91282b0caf04"), + calculateInApps(25, abtests, fourInapps) + ) + assertEquals( + setOf("d1b312bd-aa5c-414c-a0d8-8126376a2a9b"), + calculateInApps(35, abtests, fourInapps) + ) + assertEquals( + fourInapps.toSet(), + calculateInApps(75, abtests, fourInapps) + ) + } + + @Test + fun `two abtests logic with config of three inapps`() = runTest { + val abtests = listOf( + abtest.copy( + salt = "saLt1", + variants = listOf( + variant.copy( + lower = 0, + upper = 25, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf() + ), + variant.copy( + lower = 25, + upper = 100, + kind = ABTest.Variant.VariantKind.ALL, + inapps = listOf() + ) + ) + ), + abtest.copy( + salt = "SALT2", + variants = listOf( + variant.copy( + lower = 0, + upper = 75, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf() + ), + variant.copy( + lower = 75, + upper = 100, + kind = ABTest.Variant.VariantKind.CONCRETE, + inapps = listOf("655f5ffa-de86-4224-a0bf-229fe208ed0d", "6f93e2ef-0615-4e63-9c80-24bcb9e83b83") + ) + ) + ) + ) + + val mixer24and74 = mockk { + every { stringModulusHash(any(), "SALT1") } returns (24) + every { stringModulusHash(any(), "SALT2") } returns (74) + } + + assertEquals( + setOf(), + calculateInApps(mixer24and74, abtests, threeInapps) + ) + + val mixer24and99 = mockk { + every { stringModulusHash(any(), "SALT1") } returns (24) + every { stringModulusHash(any(), "SALT2") } returns (99) + } + assertEquals( + setOf(), + calculateInApps(mixer24and99, abtests, threeInapps) + ) + + val mixer99and74 = mockk { + every { stringModulusHash(any(), "SALT1") } returns (99) + every { stringModulusHash(any(), "SALT2") } returns (74) + } + assertEquals( + setOf("b33ca779-3c99-481f-ad46-91282b0caf04"), + calculateInApps(mixer99and74, abtests, threeInapps) + ) + + val mixer99and99 = mockk { + every { stringModulusHash(any(), "SALT1") } returns (99) + every { stringModulusHash(any(), "SALT2") } returns (99) + } + assertEquals( + threeInapps.toSet(), + calculateInApps(mixer99and99, abtests, threeInapps) + ) + } + + private suspend fun calculateInApps( + hash: Int, + abtests: List, + inapps: List + ): Set { + val repository: MobileConfigRepository = mockk { + coEvery { getABTests() } returns (abtests) + } + val mixer: CustomerAbMixer = mockk { + every { stringModulusHash(any(), any()) } returns (hash) + } + return InAppABTestLogic(mixer, repository).getInAppsPool(inapps) + } + + private suspend fun calculateInApps( + mixer: CustomerAbMixer, + abtests: List, + inapps: List + ): Set { + val repository: MobileConfigRepository = mockk { + coEvery { getABTests() } returns (abtests) + } + return InAppABTestLogic(mixer, repository).getInAppsPool(inapps) + } +} \ No newline at end of file diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImplTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImplTest.kt index 8ac655fef..82cbd2d03 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImplTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImplTest.kt @@ -27,6 +27,7 @@ class MobileConfigSerializationManagerImplTest { ), variants = listOf( ABTestDto.VariantDto( + id = "3162b011-b30f-4300-a72b-bd5cac0d6607", modulus = ABTestDto.VariantDto.ModulusDto( lower = 0, upper = 50, @@ -43,6 +44,7 @@ class MobileConfigSerializationManagerImplTest { ), ), ABTestDto.VariantDto( + id = "dbc39dce-db4f-4dc9-9133-378df018233b", modulus = ABTestDto.VariantDto.ModulusDto( lower = 50, upper = 100, diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/ABTestValidatorTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/ABTestValidatorTest.kt index e58164809..95166c1c3 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/ABTestValidatorTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/ABTestValidatorTest.kt @@ -27,10 +27,12 @@ internal class ABTestValidatorTest( inapps = listOf() ) val variant1 = ABTestDto.VariantDto( + id = "1", modulus = modulus.copy(upper = 50), objects = listOf(abObject) ) val variant2 = ABTestDto.VariantDto( + id = "2", modulus = modulus.copy(lower = 50), objects = listOf(abObject.copy(kind = "concrete")) ) diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidatorTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidatorTest.kt index d62ed5c2c..15f9c4894 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidatorTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/validators/VariantValidatorTest.kt @@ -24,6 +24,7 @@ internal class VariantValidatorTest( inapps = listOf() ) val variant = ABTestDto.VariantDto( + id = "dsa", modulus = ABTestDto.VariantDto.ModulusDto( lower = 0, upper = 100 @@ -42,6 +43,8 @@ internal class VariantValidatorTest( variant.copy(objects = listOf(objectDto.copy(inapps = listOf("123")))) to true, null to false, + variant.copy(id = "") to false, + variant.copy(id = " ") to false, variant.copy(modulus = null) to false, variant.copy(objects = null) to false, variant.copy(modulus = variant.modulus.copy(lower = -100)) to false, diff --git a/sdk/src/test/resources/abtests.json b/sdk/src/test/resources/abtests.json index 3639b8d44..9dbfd7daf 100644 --- a/sdk/src/test/resources/abtests.json +++ b/sdk/src/test/resources/abtests.json @@ -9,6 +9,7 @@ "salt": "c0e2682c-3d0f-4291-9308-9e48a16eb3c8", "variants": [ { + "id": "3162b011-b30f-4300-a72b-bd5cac0d6607", "modulus": { "lower": 0, "upper": 50 @@ -22,6 +23,7 @@ ] }, { + "id": "dbc39dce-db4f-4dc9-9133-378df018233b", "modulus": { "lower": 50, "upper": 100