Updated on 2026-08-14
This commit is contained in:
commit
ff24b00af4
404 changed files with 12634 additions and 3818 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
import com.squareup.kotlinpoet.*
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import org.json.JSONArray
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
|
|
@ -9,8 +11,24 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.squareup:kotlinpoet:1.15.0")
|
||||
classpath("org.json:json:20231013")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.configtoggle"
|
||||
sourceSets["main"].java.srcDir("build/generated/source/toggles")
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -32,7 +50,72 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/feature_toggles_config.json",
|
||||
generatedFileName = "FeatureToggles",
|
||||
)
|
||||
}
|
||||
|
||||
val generateExcludedBlockchainToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/excluded_blockchains_config.json",
|
||||
generatedFileName = "ExcludedBlockchainToggles",
|
||||
)
|
||||
}
|
||||
|
||||
fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
||||
val inputFile = file(inputFilePath)
|
||||
val outputDir = file("build/generated/source/toggles")
|
||||
|
||||
inputs.file(inputFile)
|
||||
outputs.dir(outputDir)
|
||||
|
||||
doLast {
|
||||
val jsonText = inputFile.readText()
|
||||
val jsonArray = JSONArray(jsonText)
|
||||
|
||||
val entries = (0 until jsonArray.length()).map { i ->
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val name = obj.getString("name")
|
||||
val version = obj.getString("version")
|
||||
CodeBlock.of("%S to %S", name, version)
|
||||
}
|
||||
|
||||
val mapInitializer = CodeBlock.builder()
|
||||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
add(entry)
|
||||
if (index != entries.lastIndex) add(",\n") else add("\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
.add(")")
|
||||
.build()
|
||||
|
||||
val objectBuilder = TypeSpec.objectBuilder(name = generatedFileName)
|
||||
.addKdoc("Generated from $inputFilePath")
|
||||
.addProperty(
|
||||
PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING))
|
||||
.initializer(mapInitializer)
|
||||
.build()
|
||||
)
|
||||
|
||||
val fileSpec = FileSpec.builder(packageName = "com.tangem.core.configtoggle", fileName = generatedFileName)
|
||||
.addType(objectBuilder.build())
|
||||
.build()
|
||||
|
||||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,10 +43,6 @@
|
|||
"name": "SEND_REDESIGN_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_BALANCE_FETCHER_ENABLED",
|
||||
"version": "5.27.0"
|
||||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -62,5 +58,17 @@
|
|||
{
|
||||
"name": "NEW_TOKEN_RECEIVE_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "YIELD_LENDING_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,4 @@ package com.tangem.core.configtoggle.blockchain
|
|||
interface ExcludedBlockchainsManager {
|
||||
|
||||
val excludedBlockchainsIds: Set<String>
|
||||
|
||||
suspend fun init()
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
internal class DefaultExcludedBlockchainsManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private var isInitialized: Boolean = false
|
||||
|
||||
private lateinit var currentExcludedBlockchains: MutableMap<String, Boolean>
|
||||
private lateinit var localExcludedBlockchains: Map<String, Boolean>
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() {
|
||||
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
|
||||
|
||||
return currentExcludedBlockchains
|
||||
.filterValues { it }
|
||||
.keys
|
||||
}
|
||||
|
||||
override suspend fun init() {
|
||||
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
|
||||
|
||||
val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync<Boolean>(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
)
|
||||
|
||||
localExcludedBlockchains = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
.mapValues { (_, isIncluded) -> !isIncluded }
|
||||
|
||||
currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys)
|
||||
.fold(mutableMapOf()) { acc, blockchainId ->
|
||||
val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId]
|
||||
|
||||
requireNotNull(isExcluded) {
|
||||
"Unable to find $blockchainId in local or stored excluded blockchains"
|
||||
}
|
||||
|
||||
acc[blockchainId] = isExcluded
|
||||
acc
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
currentExcludedBlockchains[mainnetId] = isExcluded
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return currentExcludedBlockchains == localExcludedBlockchains
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
currentExcludedBlockchains = localExcludedBlockchains.toMutableMap()
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
private suspend fun storeCurrent() {
|
||||
appPreferencesStore.storeObjectMap(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
value = currentExcludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* [MutableExcludedBlockchainsManager] implementation in dev or mocked build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property localTogglesStorage local storage for blockchain toggles
|
||||
*/
|
||||
internal class DevExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val localTogglesStorage: LocalTogglesStorage,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private val fileBlockchainToggles: Map<String, Boolean> = getFileBlockchainToggles()
|
||||
private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() = blockchainTogglesMap.filterValues { !it }.keys
|
||||
|
||||
init {
|
||||
val savedExcludedBlockchains = runBlocking { localTogglesStorage.getSyncOrEmpty() }
|
||||
|
||||
blockchainTogglesMap = fileBlockchainToggles
|
||||
.mapValues { (blockchainId, isEnabled) ->
|
||||
savedExcludedBlockchains[blockchainId] ?: isEnabled
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
blockchainTogglesMap[mainnetId] = isExcluded
|
||||
|
||||
localTogglesStorage.store(blockchainTogglesMap)
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return blockchainTogglesMap == fileBlockchainToggles
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
blockchainTogglesMap = fileBlockchainToggles.toMutableMap()
|
||||
|
||||
localTogglesStorage.store(blockchainTogglesMap)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return blockchainTogglesMap
|
||||
.filterKeys { it.isNotEmpty() }
|
||||
.toTableString(tableName = this@DevExcludedBlockchainsManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun getFileBlockchainToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* [ExcludedBlockchainsManager] implementation in PROD build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
) : ExcludedBlockchainsManager {
|
||||
|
||||
override val excludedBlockchainsIds: Set<String> = getBlockchainToggles()
|
||||
|
||||
private fun getBlockchainToggles(): Set<String> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
.filterValues { !it }
|
||||
.keys
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,10 @@ package com.tangem.core.configtoggle.di
|
|||
import android.content.Context
|
||||
import com.tangem.core.configtoggle.BuildConfig
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DevExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.ProdExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -24,26 +23,20 @@ internal object ExcludedBlockchainsManagerModule {
|
|||
@Singleton
|
||||
fun provideExcludedBlockchainsManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): ExcludedBlockchainsManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return DefaultExcludedBlockchainsManager(
|
||||
localTogglesStorage,
|
||||
appPreferencesStore,
|
||||
versionProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMutableExcludedBlockchainsManager(
|
||||
manager: ExcludedBlockchainsManager,
|
||||
): MutableExcludedBlockchainsManager? {
|
||||
if (!BuildConfig.TESTER_MENU_ENABLED) return null
|
||||
|
||||
return manager as MutableExcludedBlockchainsManager
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevExcludedBlockchainsManager(
|
||||
versionProvider = versionProvider,
|
||||
localTogglesStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdExcludedBlockchainsManager(versionProvider = versionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,12 @@ import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
|||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -25,29 +23,20 @@ internal object FeatureTogglesManagerModule {
|
|||
@Singleton
|
||||
fun provideFeatureTogglesManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): FeatureTogglesManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
versionProvider = versionProvider,
|
||||
featureTogglesLocalStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
}.also {
|
||||
// We need to initialize during the hilt graph creation
|
||||
// in order to provide the feature toggles correctly to other dependencies.
|
||||
runBlocking {
|
||||
it.init()
|
||||
}
|
||||
ProdFeatureTogglesManager(versionProvider = versionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,6 @@ package com.tangem.core.configtoggle.feature
|
|||
*/
|
||||
interface FeatureTogglesManager {
|
||||
|
||||
/** Initialize manager */
|
||||
suspend fun init()
|
||||
|
||||
/** Check feature toggle availability by name [name] */
|
||||
fun isFeatureEnabled(name: String): Boolean
|
||||
}
|
||||
|
|
@ -1,77 +1,68 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in DEV build
|
||||
* Feature toggles manager implementation in dev or mocked build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property appPreferencesStore application local store
|
||||
* @property versionProvider application version provider
|
||||
* @property featureTogglesLocalStorage local storage for feature toggles
|
||||
*/
|
||||
internal class DevFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
private var featureTogglesMap: MutableMap<String, Boolean>? = null
|
||||
private var localFeatureTogglesMap: Map<String, Boolean>? = null
|
||||
private var fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override suspend fun init() {
|
||||
if (featureTogglesMap != null && localFeatureTogglesMap != null) {
|
||||
return // Already initialized
|
||||
}
|
||||
init {
|
||||
val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
||||
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
||||
) ?: emptyMap<String, Boolean>()
|
||||
|
||||
val localFeatureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
|
||||
localFeatureTogglesMap = localFeatureToggles
|
||||
|
||||
featureTogglesMap = localFeatureToggles
|
||||
featureTogglesMap = fileFeatureTogglesMap
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] == true
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap!!
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == fileFeatureTogglesMap
|
||||
|
||||
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
||||
featureTogglesMap!![name] ?: return
|
||||
featureTogglesMap!![name] = isEnabled
|
||||
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!)
|
||||
featureTogglesMap[name] ?: return
|
||||
featureTogglesMap[name] = isEnabled
|
||||
featureTogglesLocalStorage.store(value = featureTogglesMap)
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
featureTogglesMap = localFeatureTogglesMap!!.toMutableMap()
|
||||
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!)
|
||||
featureTogglesMap = fileFeatureTogglesMap.toMutableMap()
|
||||
featureTogglesLocalStorage.store(value = fileFeatureTogglesMap)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return featureTogglesMap.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
||||
featureTogglesMap = map
|
||||
}
|
||||
|
||||
private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map<String, Boolean>) {
|
||||
storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,30 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in PROD build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property versionProvider application version provider
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : FeatureTogglesManager {
|
||||
|
||||
private var featureToggles: Map<String, Boolean>? = null
|
||||
private val featureToggles: Map<String, Boolean> = getFileFeatureToggles()
|
||||
|
||||
override suspend fun init() {
|
||||
if (featureToggles != null) {
|
||||
return // Already initialized
|
||||
}
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] == true
|
||||
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
featureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getProdFeatureToggles() = featureToggles!!
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setProdFeatureToggles(map: Map<String, Boolean>) {
|
||||
featureToggles = map
|
||||
}
|
||||
fun getProdFeatureToggles() = featureToggles
|
||||
}
|
||||
|
|
@ -1,24 +1,34 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import kotlin.properties.Delegates
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
/**
|
||||
* Storage implementation for storing local feature toggles.
|
||||
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
|
||||
* Local storage for toggles
|
||||
*
|
||||
* @property assetLoader asset loader
|
||||
* @property appPreferencesStore app preferences store
|
||||
* @property preferencesKey preferences key
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class LocalTogglesStorage(
|
||||
private val assetLoader: AssetLoader,
|
||||
) : TogglesStorage {
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val preferencesKey: Preferences.Key<String>,
|
||||
) {
|
||||
|
||||
override var toggles: List<ConfigToggle> by Delegates.notNull()
|
||||
private set
|
||||
suspend fun getSyncOrEmpty(): Map<String, Boolean> {
|
||||
return appPreferencesStore.getObjectMapSync<Boolean>(key = preferencesKey)
|
||||
}
|
||||
|
||||
override suspend fun populate(path: String) {
|
||||
toggles = assetLoader.loadList<ConfigToggle>(path)
|
||||
suspend fun store(value: Map<String, Boolean>) {
|
||||
appPreferencesStore.storeObjectMap(key = preferencesKey, value = value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
|
||||
|
||||
internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
|
||||
return associate { localToggle ->
|
||||
Pair(
|
||||
first = localToggle.name,
|
||||
second = VersionAvailabilityContract(currentVersion, localToggle.version),
|
||||
)
|
||||
internal fun Map<String, String>.defineTogglesAvailability(appVersion: String?): Map<String, Boolean> {
|
||||
return if (appVersion == null) {
|
||||
mapValues { false }
|
||||
} else {
|
||||
mapValues { (_, version) ->
|
||||
VersionAvailabilityContract(currentVersion = appVersion, localVersion = version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal fun Map<String, Boolean>.toTableString(tableName: String): String {
|
||||
return buildString {
|
||||
append("$tableName:\n")
|
||||
append("|------------------------------------------|-----------|\n")
|
||||
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled"))
|
||||
append("|------------------------------------------|-----------|\n")
|
||||
entries.forEachIndexed { index, (name, isEnabled) ->
|
||||
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled))
|
||||
}
|
||||
append("|------------------------------------------|-----------|")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val localTogglesStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedToggles = mapOf("CHAIN_1" to false, "CHAIN_2" to true)
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_2")
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ExcludeBlockchain {
|
||||
|
||||
@Test
|
||||
fun excludeBlockchain_changesStatusAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
|
||||
// Act
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).contains("CHAIN_1")
|
||||
coVerify { localTogglesStorage.store(match { it["CHAIN_1"] == false }) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsTrueIfMatchesFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsFalseIfDiffersFromFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig_resetsToFileAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val toggles = mapOf("CHAIN_1" to "2.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns mapOf("CHAIN_1" to true)
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.recoverLocalConfig()
|
||||
|
||||
// Act
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize excluded blockchains`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns ""
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdExcludedBlockchainsManager(versionProvider) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val featureTogglesLocalStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
coVerify(inverse = true) { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = emptyMap<String, Boolean>()
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns unknown toggles`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_3" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsFeatureEnabled {
|
||||
|
||||
private lateinit var manager: DevFeatureTogglesManager
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isMatchLocalConfig(model: IsMatchLocalConfigModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.fileFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).apply {
|
||||
setFeatureToggles(model.storedFeatureToggles.toMutableMap())
|
||||
}
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_3" to true, "TOGGLE_4" to false),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = emptyMap(),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsMatchLocalConfigModel(
|
||||
val fileFeatureToggles: Map<String, String>,
|
||||
val storedFeatureToggles: Map<String, Boolean>,
|
||||
val expected: Boolean,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetFeatureToggles {
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ChangeToggle {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun changeToggle(model: ChangeToggleModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.initialToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.changeToggle(name = model.name, isEnabled = model.isEnabled)
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = model.expectedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
|
||||
if (model.expectedStoreSaving) {
|
||||
featureTogglesLocalStorage.store(model.expectedToggles)
|
||||
}
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = false,
|
||||
expectedToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_2",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_3",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = emptyMap(),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = true,
|
||||
expectedToggles = emptyMap(),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.recoverLocalConfig()
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
featureTogglesLocalStorage.store(expected)
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeToggleModel(
|
||||
val initialToggles: Map<String, String>,
|
||||
val name: String,
|
||||
val isEnabled: Boolean,
|
||||
val expectedToggles: Map<String, Boolean>,
|
||||
val expectedStoreSaving: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class DevTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val appPreferenceStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = mockk(relaxed = true),
|
||||
)
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferenceStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns savedFeatureTogglesMap
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns emptyMap()
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns null
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues(Map.Entry<String, Boolean>::value)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion = "")
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() {
|
||||
val expected = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(expected)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that contains in map`() = runTest {
|
||||
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
|
||||
val resultMap = mutableMapOf(
|
||||
changeableToggleName to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle(changeableToggleName, true)
|
||||
|
||||
resultMap[changeableToggleName] = true
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that doesn't contains in map`() = runTest {
|
||||
val resultMap = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle("FEATURE_TOGGLE", true)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val savedFeatureToggles = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val savedFeatureTogglesMap = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdFeatureTogglesManager(versionProvider) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsFeatureEnabled {
|
||||
|
||||
private lateinit var manager: ProdFeatureTogglesManager
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
manager = ProdFeatureTogglesManager(versionProvider)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsFeatureEnabledModel(val name: String, val expected: Boolean)
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ProdTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles.associateToggles(currentVersion)
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } throws IllegalStateException(
|
||||
"Property featureToggles should be initialized before get.",
|
||||
)
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("localFeatureToggles shouldn't be initialized") }
|
||||
.onFailure {
|
||||
Truth
|
||||
.assertThat(it)
|
||||
.hasMessageThat()
|
||||
.contains("Property featureToggles should be initialized before get.")
|
||||
|
||||
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) }
|
||||
verifyAll(inverse = true) { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } throws PackageManager.NameNotFoundException()
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("versionProvider should throws exception") }
|
||||
.onFailure {
|
||||
Truth.assertThat(it).isInstanceOf(PackageManager.NameNotFoundException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
|
||||
val disabledFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class LocalTogglesStorageTest {
|
||||
|
||||
private val assetReader = mockk<AssetReader>()
|
||||
private val moshi = mockk<Moshi>()
|
||||
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
|
||||
|
||||
// Impossible to mockk AssetLoader because it implement inline functions
|
||||
private val assetLoader = AssetLoader(
|
||||
assetReader = assetReader,
|
||||
moshi = moshi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val storage = LocalTogglesStorage(assetLoader)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() returns featureToggles
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if assetReader throws exception`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
|
||||
everyReadingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
|
||||
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
|
||||
|
||||
private fun everyCreatingMoshiAdapter() = every {
|
||||
val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java)
|
||||
moshi.adapter<List<ConfigToggle>>(types)
|
||||
}
|
||||
|
||||
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
|
||||
|
||||
private companion object {
|
||||
const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json"
|
||||
|
||||
val json = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val featureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue