Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-29 14:52:25 +04:00
parent 665c0ff763
commit d48ba8a8bd
13 changed files with 829 additions and 459 deletions

View file

@ -77,6 +77,7 @@ import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints import dagger.hilt.EntryPoints
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.rekotlin.Store import org.rekotlin.Store
import timber.log.Timber
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState> lateinit var store: Store<AppState>
@ -289,13 +290,17 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
tangemAppLoggerInitializer.initialize() tangemAppLoggerInitializer.initialize()
Timber.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
}
foregroundActivityObserver = ForegroundActivityObserver() foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them. // We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking { runBlocking {
awaitAll( awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() }, async { excludedBlockchainsManager.init() },
) )
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())

View file

@ -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 { plugins {
alias(deps.plugins.android.library) alias(deps.plugins.android.library)
@ -9,8 +11,24 @@ plugins {
id("configuration") id("configuration")
} }
buildscript {
dependencies {
classpath("com.squareup:kotlinpoet:1.15.0")
classpath("org.json:json:20231013")
}
}
android { android {
namespace = "com.tangem.core.configtoggle" 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 { dependencies {
@ -32,7 +50,72 @@ dependencies {
implementation(projects.core.utils) implementation(projects.core.utils)
testImplementation(deps.test.coroutine) testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit) testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk) testImplementation(deps.test.mockk)
testImplementation(deps.test.truth) 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)
}
} }

View file

@ -5,16 +5,14 @@ import com.tangem.core.configtoggle.BuildConfig
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage
import com.tangem.core.configtoggle.version.DefaultVersionProvider import com.tangem.core.configtoggle.version.DefaultVersionProvider
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -25,29 +23,17 @@ internal object FeatureTogglesManagerModule {
@Singleton @Singleton
fun provideFeatureTogglesManager( fun provideFeatureTogglesManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
assetLoader: AssetLoader,
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager { ): FeatureTogglesManager {
val localTogglesStorage = LocalTogglesStorage(assetLoader)
val versionProvider = DefaultVersionProvider(context) val versionProvider = DefaultVersionProvider(context)
return if (BuildConfig.TESTER_MENU_ENABLED) { return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager( DevFeatureTogglesManager(
localTogglesStorage = localTogglesStorage,
appPreferencesStore = appPreferencesStore,
versionProvider = versionProvider, versionProvider = versionProvider,
featureTogglesLocalStorage = FeatureTogglesLocalStorage(appPreferencesStore),
) )
} else { } else {
ProdFeatureTogglesManager( ProdFeatureTogglesManager(versionProvider = versionProvider)
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()
}
} }
} }
} }

View file

@ -7,9 +7,6 @@ package com.tangem.core.configtoggle.feature
*/ */
interface FeatureTogglesManager { interface FeatureTogglesManager {
/** Initialize manager */
suspend fun init()
/** Check feature toggle availability by name [name] */ /** Check feature toggle availability by name [name] */
fun isFeatureEnabled(name: String): Boolean fun isFeatureEnabled(name: String): Boolean
} }

View file

@ -1,77 +1,77 @@
package com.tangem.core.configtoggle.feature.impl package com.tangem.core.configtoggle.feature.impl
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage
import com.tangem.core.configtoggle.utils.associateToggles import com.tangem.core.configtoggle.utils.defineTogglesAvailability
import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore import kotlinx.coroutines.runBlocking
import com.tangem.datasource.local.preferences.PreferencesKeys import java.util.Locale
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import kotlin.properties.Delegates
import com.tangem.datasource.local.preferences.utils.storeObject
/** /**
* Feature toggles manager implementation in DEV build * Feature toggles manager implementation in DEV build
* *
* @property localTogglesStorage local feature toggles storage
* @property appPreferencesStore application local store
* @property versionProvider application version provider * @property versionProvider application version provider
* @property featureTogglesLocalStorage local storage for feature toggles
*/ */
internal class DevFeatureTogglesManager( internal class DevFeatureTogglesManager(
private val localTogglesStorage: TogglesStorage,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider, private val versionProvider: VersionProvider,
private val featureTogglesLocalStorage: FeatureTogglesLocalStorage,
) : MutableFeatureTogglesManager { ) : MutableFeatureTogglesManager {
private var featureTogglesMap: MutableMap<String, Boolean>? = null private var fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
private var localFeatureTogglesMap: Map<String, Boolean>? = null private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
override suspend fun init() { init {
if (featureTogglesMap != null && localFeatureTogglesMap != null) { val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() }
return // Already initialized
}
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) featureTogglesMap = fileFeatureTogglesMap
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
.mapValues { resultToggle -> .mapValues { resultToggle ->
savedFeatureToggles[resultToggle.key] ?: resultToggle.value savedFeatureToggles[resultToggle.key] ?: resultToggle.value
} }
.toMutableMap() .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) { override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap!![name] ?: return featureTogglesMap[name] ?: return
featureTogglesMap!![name] = isEnabled featureTogglesMap[name] = isEnabled
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!) featureTogglesLocalStorage.store(value = featureTogglesMap)
} }
override suspend fun recoverLocalConfig() { override suspend fun recoverLocalConfig() {
featureTogglesMap = localFeatureTogglesMap!!.toMutableMap() featureTogglesMap = fileFeatureTogglesMap.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!) featureTogglesLocalStorage.store(value = fileFeatureTogglesMap)
}
override fun toString(): String {
return buildString {
append("DevFeatureTogglesManager:\n")
append("|------------------------------------------|-----------|\n")
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled"))
append("|------------------------------------------|-----------|\n")
featureTogglesMap.entries.forEachIndexed { index, (name, isEnabled) ->
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled))
}
append("|------------------------------------------|-----------|")
}
}
private fun getFileFeatureToggles(): Map<String, Boolean> {
val appVersion = versionProvider.get()
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
} }
@VisibleForTesting(otherwise = VisibleForTesting.NONE) @VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun setFeatureToggles(map: MutableMap<String, Boolean>) { fun setFeatureToggles(map: MutableMap<String, Boolean>) {
featureTogglesMap = map featureTogglesMap = map
} }
private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map<String, Boolean>) {
storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value)
}
} }

View file

@ -1,41 +1,30 @@
package com.tangem.core.configtoggle.feature.impl package com.tangem.core.configtoggle.feature.impl
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage import com.tangem.core.configtoggle.utils.defineTogglesAvailability
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.core.configtoggle.version.VersionProvider
/** /**
* Feature toggles manager implementation in PROD build * 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( internal class ProdFeatureTogglesManager(
private val localTogglesStorage: TogglesStorage,
private val versionProvider: VersionProvider, private val versionProvider: VersionProvider,
) : FeatureTogglesManager { ) : FeatureTogglesManager {
private var featureToggles: Map<String, Boolean>? = null private val featureToggles: Map<String, Boolean> = getFileFeatureToggles()
override suspend fun init() { override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] == true
if (featureToggles != null) {
return // Already initialized
}
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) private fun getFileFeatureToggles(): Map<String, Boolean> {
featureToggles = localTogglesStorage.toggles val appVersion = versionProvider.get()
.associateToggles(currentVersion = versionProvider.get() ?: "")
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
} }
override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false
@VisibleForTesting(otherwise = VisibleForTesting.NONE) @VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getProdFeatureToggles() = featureToggles!! fun getProdFeatureToggles() = featureToggles
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun setProdFeatureToggles(map: Map<String, Boolean>) {
featureToggles = map
}
} }

View file

@ -0,0 +1,31 @@
package com.tangem.core.configtoggle.storage
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
/**
* Local storage for feature toggles
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class FeatureTogglesLocalStorage(
private val appPreferencesStore: AppPreferencesStore,
) {
suspend fun getSyncOrEmpty(): Map<String, Boolean> {
return appPreferencesStore.getObjectMapSync<Boolean>(key = FEATURE_TOGGLES_KEY)
}
suspend fun store(value: Map<String, Boolean>) {
appPreferencesStore.storeObjectMap(key = FEATURE_TOGGLES_KEY, value = value)
}
private companion object {
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
}
}

View file

@ -10,4 +10,14 @@ internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<St
second = VersionAvailabilityContract(currentVersion, localToggle.version), 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)
}
}
} }

View file

@ -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.FeatureTogglesLocalStorage
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<FeatureTogglesLocalStorage>(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,
)
}

View file

@ -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"),
)
}
}

View file

@ -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)
}

View file

@ -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,
)
}
}

View file

@ -56,8 +56,6 @@ object PreferencesKeys {
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") } val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }