Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 17:39:17 +04:00
parent dfc3dbc324
commit 513e0a4d7c
20 changed files with 460 additions and 294 deletions

View file

@ -293,16 +293,13 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
Timber.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
}
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { excludedBlockchainsManager.init() },
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}

View file

@ -24,7 +24,7 @@ android {
}
tasks.named("preBuild") {
dependsOn(generateFeatureToggles /*generateExcludedBlockchainToggles*/)
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles)
}
tasks.withType<Test>().configureEach {

View file

@ -3,6 +3,4 @@ package com.tangem.core.configtoggle.blockchain
interface ExcludedBlockchainsManager {
val excludedBlockchainsIds: Set<String>
suspend fun init()
}

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import com.tangem.core.configtoggle.BuildConfig
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.configtoggle.storage.FeatureTogglesLocalStorage
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
import com.tangem.core.configtoggle.version.DefaultVersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
@ -30,7 +30,10 @@ internal object FeatureTogglesManagerModule {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager(
versionProvider = versionProvider,
featureTogglesLocalStorage = FeatureTogglesLocalStorage(appPreferencesStore),
featureTogglesLocalStorage = LocalTogglesStorage(
appPreferencesStore = appPreferencesStore,
preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY,
),
)
} else {
ProdFeatureTogglesManager(versionProvider = versionProvider)

View file

@ -3,11 +3,11 @@ 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.FeatureTogglesLocalStorage
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 java.util.Locale
import kotlin.properties.Delegates
/**
@ -18,7 +18,7 @@ import kotlin.properties.Delegates
*/
internal class DevFeatureTogglesManager(
private val versionProvider: VersionProvider,
private val featureTogglesLocalStorage: FeatureTogglesLocalStorage,
private val featureTogglesLocalStorage: LocalTogglesStorage,
) : MutableFeatureTogglesManager {
private var fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
@ -52,16 +52,7 @@ internal class DevFeatureTogglesManager(
}
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("|------------------------------------------|-----------|")
}
return featureTogglesMap.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName)
}
private fun getFileFeatureToggles(): Map<String, Boolean> {

View file

@ -1,31 +0,0 @@
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

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

View file

@ -1,17 +1,7 @@
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 }

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ 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.storage.LocalTogglesStorage
import com.tangem.core.configtoggle.version.VersionProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
@ -19,7 +19,7 @@ import org.junit.jupiter.params.ParameterizedTest
internal class DevFeatureTogglesManagerTest {
private val versionProvider = mockk<VersionProvider>()
private val featureTogglesLocalStorage = mockk<FeatureTogglesLocalStorage>(relaxUnitFun = true)
private val featureTogglesLocalStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)

View file

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

View file

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

View file

@ -3,6 +3,7 @@ package com.tangem.feature.tester.presentation.excludedblockchains
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.ui.components.fields.entity.SearchBarUM
@ -25,11 +26,11 @@ import javax.inject.Inject
@HiltViewModel
internal class ExcludedBlockchainsViewModel @Inject constructor(
private val appVersionProvider: AppVersionProvider,
excludedBlockchainsManager: MutableExcludedBlockchainsManager?,
excludedBlockchainsManager: ExcludedBlockchainsManager,
) : ViewModel() {
private val excludedBlockchainsManager: MutableExcludedBlockchainsManager =
requireNotNull(excludedBlockchainsManager) {
requireNotNull(excludedBlockchainsManager as? MutableExcludedBlockchainsManager) {
"Mutable excluded blockchains manager can't be null when tester actions is available"
}

View file

@ -28,10 +28,6 @@ class ExcludedBlockchains @Inject internal constructor(
excludedBlockchainsManager = object : ExcludedBlockchainsManager {
override val excludedBlockchainsIds: Set<String> = emptySet()
override suspend fun init() {
/* no-op */
}
},
)