Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-18 17:27:45 +04:00
parent e72c2c87bc
commit 6dbd7bd861
58 changed files with 207 additions and 206 deletions

View file

@ -0,0 +1,10 @@
[
{
"mainnet_id": "",
"excluded_till": "undefined"
},
{
"mainnet_id": "NEXA",
"excluded_till": "undefined"
}
]

View file

@ -0,0 +1,46 @@
package com.tangem.core.toggle.di
import android.content.Context
import com.tangem.core.toggle.feature.FeatureTogglesManager
import com.tangem.core.toggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.toggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.toggle.storage.LocalTogglesStorage
import com.tangem.core.toggle.version.DefaultVersionProvider
import com.tangem.core.toggles.BuildConfig
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 javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureTogglesManagerModule {
@Provides
@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,
)
} else {
ProdFeatureTogglesManager(
localTogglesStorage = localTogglesStorage,
versionProvider = versionProvider,
)
}
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.toggle.feature
/**
* Component for getting information about the availability of feature toggles
*
[REDACTED_AUTHOR]
*/
interface FeatureTogglesManager {
/** Initialize manager */
suspend fun init()
/** Check feature toggle availability by name [name] */
fun isFeatureEnabled(name: String): Boolean
}

View file

@ -0,0 +1,21 @@
package com.tangem.core.toggle.feature
/**
* Component for change information about the availability of feature toggles
*
[REDACTED_AUTHOR]
*/
interface MutableFeatureTogglesManager : FeatureTogglesManager {
/** Check if the current state of the feature toggles matches the local config state. */
fun isMatchLocalConfig(): Boolean
/** Get feature toggles */
fun getFeatureToggles(): Map<String, Boolean>
/** Change availability [isEnabled] of toggle with name [name] */
suspend fun changeToggle(name: String, isEnabled: Boolean)
/** Recover local config state */
suspend fun recoverLocalConfig()
}

View file

@ -0,0 +1,74 @@
package com.tangem.core.toggle.feature.impl
import androidx.annotation.VisibleForTesting
import com.tangem.core.toggle.feature.MutableFeatureTogglesManager
import com.tangem.core.toggle.storage.TogglesStorage
import com.tangem.core.toggle.utils.associateToggles
import com.tangem.core.toggle.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 kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
*
* @property localTogglesStorage local feature toggles storage
* @property appPreferencesStore application local store
* @property versionProvider application version provider
*/
internal class DevFeatureTogglesManager(
private val localTogglesStorage: TogglesStorage,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
val localFeatureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
localFeatureTogglesMap = localFeatureToggles
featureTogglesMap = localFeatureToggles
.mapValues { resultToggle ->
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
}
.toMutableMap()
}
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap)
}
override suspend fun recoverLocalConfig() {
featureTogglesMap = localFeatureTogglesMap.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap)
}
@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)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.toggle.feature.impl
internal object FeatureTogglesConstants {
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
}

View file

@ -0,0 +1,38 @@
package com.tangem.core.toggle.feature.impl
import androidx.annotation.VisibleForTesting
import com.tangem.core.toggle.feature.FeatureTogglesManager
import com.tangem.core.toggle.storage.TogglesStorage
import com.tangem.core.toggle.utils.associateToggles
import com.tangem.core.toggle.version.VersionProvider
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in PROD build
*
* @property localTogglesStorage local feature toggles storage
* @property versionProvider application version provider
*/
internal class ProdFeatureTogglesManager(
private val localTogglesStorage: TogglesStorage,
private val versionProvider: VersionProvider,
) : FeatureTogglesManager {
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
override suspend fun init() {
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
featureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get() ?: "")
}
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
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.core.toggle.storage
import com.tangem.datasource.asset.loader.AssetLoader
import kotlin.properties.Delegates
/**
* Storage implementation for storing local feature toggles.
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
*
* @property assetLoader asset loader
*
[REDACTED_AUTHOR]
*/
internal class LocalTogglesStorage(
private val assetLoader: AssetLoader,
) : TogglesStorage {
override var toggles: List<Toggle> by Delegates.notNull()
private set
override suspend fun populate(path: String) {
toggles = assetLoader.loadList<Toggle>(path)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.core.toggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about config toggle
*
* @property name toggle name
* @property version version number in which the feature will be included
*
* IMPORTANT: if the version is "undefined", it means that toggle is disabled!
*
[REDACTED_AUTHOR]
*/
@JsonClass(generateAdapter = true)
internal data class Toggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.core.toggle.storage
/**
* Component that initializes and stores a list of feature toggles
*
[REDACTED_AUTHOR]
*/
internal interface TogglesStorage {
/** List of feature toggles */
val toggles: List<Toggle>
/** Populate the storage with toggles */
suspend fun populate(path: String)
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.toggle.utils
import com.tangem.core.toggle.storage.Toggle
import com.tangem.core.toggle.version.VersionAvailabilityContract
internal fun List<Toggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
return associate { localToggle ->
Pair(
first = localToggle.name,
second = VersionAvailabilityContract(currentVersion, localToggle.version),
)
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.core.toggle.version
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.tangem.utils.StringsSigns.MINUS
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
/**
* Implementation of application version provider
*
* @property context application context
*/
internal class DefaultVersionProvider @Inject constructor(
@ApplicationContext private val context: Context,
) : VersionProvider {
override fun get(): String? {
return runCatching { getVersionName().substringBefore(VERSION_NAME_DELIMITER) }
.fold(onSuccess = { it }, onFailure = { null })
}
private fun getVersionName(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager
.getPackageInfo(
context.packageName,
PackageManager.PackageInfoFlags.of(0),
)
.versionName
} else {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
}
}
private companion object {
const val VERSION_NAME_DELIMITER = MINUS
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.core.toggle.version
import androidx.annotation.VisibleForTesting
import timber.log.Timber
/**
* Presentation of application version (<major>.<minor>.<fix?>).
*
* @param value version value as string
*
[REDACTED_AUTHOR]
*/
internal class Version private constructor(value: String) : Comparable<Version> {
private val major: Int
private val minor: Int
private val fix: Int?
init {
val versions = value.split(VERSION_DELIMITER).map(String::toInt)
major = versions.getVersionValue(index = MAJOR_VERSION_POSITION)
minor = versions.getVersionValue(index = MINOR_VERSION_POSITION)
fix = versions.getOrNull(index = FIX_VERSION_POSITION)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
constructor(major: Int, minor: Int, fix: Int? = null) : this(
value = "$major.$minor${if (fix != null) ".$fix" else ""}",
)
override fun compareTo(other: Version): Int {
var result = major.compareTo(other.major)
if (result == 0) result = minor.compareTo(other.minor)
if (result == 0) {
when {
fix == null && other.fix == null -> result = 0
fix == null && other.fix != null -> result = -1
fix != null && other.fix == null -> result = 1
fix != null && other.fix != null -> result = fix.compareTo(other.fix)
}
}
return result
}
private fun List<Int>.getVersionValue(index: Int): Int {
return getOrNull(index) ?: error("Invalid version")
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getMajorVersion() = major
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getMinorVersion() = minor
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getFixVersion() = fix
companion object {
private const val MAJOR_VERSION_POSITION = 0
private const val MINOR_VERSION_POSITION = 1
private const val FIX_VERSION_POSITION = 2
private const val VERSION_DELIMITER = "."
/**
* Create instance with value [value].
* If [value] doesn't meet all requirements, the function returns null.
*/
fun create(value: String): Version? {
return try {
Version(value)
} catch (exception: Exception) {
Timber.e(exception, "Invalid version - %s", value)
return null
}
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.core.toggle.version
/**
* Version contract to evaluate availability of feature toggle
*
[REDACTED_AUTHOR]
*/
internal object VersionAvailabilityContract {
private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined"
/** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */
operator fun invoke(currentVersion: String, localVersion: String): Boolean {
if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false
val current = Version.create(currentVersion) ?: return false
val local = Version.create(localVersion) ?: return false
return current >= local
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.toggle.version
/** Application version provider */
internal interface VersionProvider {
/** Get application version */
fun get(): String?
}

View file

@ -0,0 +1,71 @@
package com.tangem.core.toggle.contract
import com.google.common.truth.Truth
import com.tangem.core.toggle.version.VersionAvailabilityContract
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class VersionAvailabilityContractTest {
@Test
fun `local version is undefined`() {
val currentVersion = "0.0.0"
val localVersion = "undefined"
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
Truth.assertThat(actual).isFalse()
}
@Test
fun `invalid current version`() {
val currentVersion = ".0.0"
val localVersion = "0.0.0"
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
Truth.assertThat(actual).isFalse()
}
@Test
fun `invalid local version`() {
val currentVersion = "0.0.0"
val localVersion = ".0.0"
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
Truth.assertThat(actual).isFalse()
}
@Test
fun `current version is greater than local version`() {
val currentVersion = "1.0.0"
val localVersion = "0.0.0"
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
Truth.assertThat(actual).isTrue()
}
@Test
fun `current version is equal to local version`() {
val currentVersion = "0.0.1"
val localVersion = "0.0.1"
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
Truth.assertThat(actual).isTrue()
}
@Test
fun `current version is less than local version`() {
val currentVersion = "0.0.0"
val localVersion = "0.1.0"
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
Truth.assertThat(actual).isFalse()
}
}

View file

@ -0,0 +1,122 @@
package com.tangem.core.toggle.contract
import com.google.common.truth.Truth
import com.tangem.core.toggle.version.Version
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class VersionTest {
@Test
fun `right versions order`() {
val major = 1
val minor = 2
val fix = 3
val versionString = "$major.$minor.$fix"
val version = Version.create(versionString)
Truth.assertThat(version?.getMajorVersion()).isEqualTo(major)
Truth.assertThat(version?.getMinorVersion()).isEqualTo(minor)
Truth.assertThat(version?.getFixVersion()).isEqualTo(fix)
}
@Test
fun `major version skipped`() {
Truth.assertThat(Version.create(".0.0")).isNull()
}
@Test
fun `minor version skipped`() {
Truth.assertThat(Version.create("0..0")).isNull()
}
@Test
fun `fix version skipped`() {
Truth.assertThat(Version.create("0.0.")).isNull()
}
@Test
fun `optional fix version`() {
val actual = Version.create("0.0")
val expected = Version(major = 0, minor = 0)
Truth.assertThat(actual).isEquivalentAccordingToCompareTo(expected)
}
@Test
fun `full form version`() {
val actual = Version.create("0.0.0")
val expected = Version(major = 0, minor = 0, fix = 0)
Truth.assertThat(actual).isEquivalentAccordingToCompareTo(expected)
}
@Test
fun `compareTo if major version of one is greater than the other`() {
val one = Version.create("1.0.0")
val other = Version(major = 0, minor = 1, fix = 1)
val actual = one?.compareTo(other)
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
}
@Test
fun `compareTo if minor version of one is greater than the other`() {
val one = Version.create("1.1.0")
val other = Version(major = 1, minor = 0, fix = 1)
val actual = one?.compareTo(other)
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
}
@Test
fun `compareTo if fix version of one is greater than the other`() {
val one = Version.create("1.1.1")
val other = Version(major = 1, minor = 1, fix = 0)
val actual = one?.compareTo(other)
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
}
@Test
fun `compareTo if one and other's fix version skipped`() {
val one = Version.create("1.1")
val other = Version(major = 1, minor = 1)
val actual = one?.compareTo(other)
Truth.assertThat(actual).isEqualTo(ONE_IS_EQUAL_TO_OTHER)
}
@Test
fun `compareTo if one's fix version skipped`() {
val one = Version.create("1.1")
val other = Version(major = 1, minor = 1, fix = 0)
val actual = one?.compareTo(other)
Truth.assertThat(actual).isEqualTo(ONE_IS_LESS_THAN_OTHER)
}
@Test
fun `compareTo if other's fix version skipped`() {
val one = Version.create("1.1.0")
val other = Version(major = 1, minor = 1)
val actual = one?.compareTo(other)
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
}
private companion object {
const val ONE_IS_GREATER_THAN_OTHER = 1
const val ONE_IS_EQUAL_TO_OTHER = 0
const val ONE_IS_LESS_THAN_OTHER = -1
}
}

View file

@ -0,0 +1,230 @@
package com.tangem.core.toggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.tangem.core.toggle.feature.impl.DevFeatureTogglesManager
import com.tangem.core.toggle.storage.Toggle
import com.tangem.core.toggle.storage.TogglesStorage
import com.tangem.core.toggle.utils.associateToggles
import com.tangem.core.toggle.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 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 = mockk<AppPreferencesStore>(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() } 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()
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() } 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()
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() } 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()
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() } 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()
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(
Toggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
Toggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
}
}

View file

@ -0,0 +1,135 @@
package com.tangem.core.toggle.manager
import android.content.pm.PackageManager
import com.google.common.truth.Truth
import com.tangem.core.toggle.feature.impl.ProdFeatureTogglesManager
import com.tangem.core.toggle.storage.Toggle
import com.tangem.core.toggle.storage.TogglesStorage
import com.tangem.core.toggle.utils.associateToggles
import com.tangem.core.toggle.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() } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } returns currentVersion
manager.init()
coVerifyOrder {
localTogglesStorage.populate()
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() } just Runs
every { localTogglesStorage.toggles } returns localFeatureToggles
every { versionProvider.get() } returns null
manager.init()
coVerifyOrder {
localTogglesStorage.populate()
versionProvider.get()
}
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles)
}
@Test
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
coEvery { localTogglesStorage.populate() } 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() }
verifyAll(inverse = true) { versionProvider.get() }
}
@Test
fun `failure initialize storage if versionProvider throws exception`() = runTest {
coEvery { localTogglesStorage.populate() } 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()
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(
Toggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
Toggle(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

@ -0,0 +1,104 @@
package com.tangem.core.toggle.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.datasource.asset.loader.AssetLoader
import com.tangem.datasource.asset.reader.AssetReader
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<Toggle>>>()
// Impossible to mockk AssetLoader because it implement inline functions
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
private val storage = LocalTogglesStorage(assetLoader)
@Test
fun `successfully initialize storage`() = runTest {
everyReadingJson() returns json
everyCreatingMoshiAdapter() returns jsonAdapter
everyMappingJson() returns featureToggles
storage.populate()
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()
coVerifyOrder {
assetReader.read(CONFIG_FILE_NAME)
jsonAdapter.fromJson(json)
}
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<Toggle>())
}
@Test
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
everyReadingJson() throws IOException()
storage.populate()
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<Toggle>())
}
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
private fun everyCreatingMoshiAdapter() = every {
val types = Types.newParameterizedType(List::class.java, Toggle::class.java)
moshi.adapter<List<Toggle>>(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(
Toggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
Toggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
)
}
}