Updated on 2026-08-14
This commit is contained in:
parent
a3a3a4a7cb
commit
a30e44b1cf
21 changed files with 736 additions and 25 deletions
|
|
@ -7,11 +7,16 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(deps.moshi)
|
implementation(project(":core:datasource"))
|
||||||
implementation(deps.moshi.kotlin)
|
|
||||||
implementation(deps.hilt.android)
|
implementation(deps.hilt.android)
|
||||||
kapt(deps.hilt.kapt)
|
kapt(deps.hilt.kapt)
|
||||||
|
implementation(deps.moshi)
|
||||||
|
implementation(deps.moshi.kotlin)
|
||||||
implementation(deps.timber)
|
implementation(deps.timber)
|
||||||
|
|
||||||
implementation(project(":core:datasource"))
|
testImplementation(deps.test.coroutine)
|
||||||
|
testImplementation(deps.test.junit)
|
||||||
|
testImplementation(deps.test.mockk)
|
||||||
|
testImplementation(deps.test.truth)
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package com.tangem.core.featuretoggle
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature toggle
|
|
||||||
*
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
interface FeatureToggle {
|
|
||||||
|
|
||||||
/** Feature toggle name */
|
|
||||||
val name: String
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.core.featuretoggle.contract
|
package com.tangem.core.featuretoggle.contract
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,6 +24,11 @@ internal class Version private constructor(value: String) : Comparable<Version>
|
||||||
fix = versions.getOrNull(index = FIX_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 {
|
override fun compareTo(other: Version): Int {
|
||||||
var result = major.compareTo(other.major)
|
var result = major.compareTo(other.major)
|
||||||
if (result == 0) result = minor.compareTo(other.minor)
|
if (result == 0) result = minor.compareTo(other.minor)
|
||||||
|
|
@ -41,6 +47,15 @@ internal class Version private constructor(value: String) : Comparable<Version>
|
||||||
return getOrNull(index) ?: error("Invalid version")
|
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 {
|
companion object {
|
||||||
private const val MAJOR_VERSION_POSITION = 0
|
private const val MAJOR_VERSION_POSITION = 0
|
||||||
private const val MINOR_VERSION_POSITION = 1
|
private const val MINOR_VERSION_POSITION = 1
|
||||||
|
|
@ -55,7 +70,7 @@ internal class Version private constructor(value: String) : Comparable<Version>
|
||||||
return try {
|
return try {
|
||||||
Version(value)
|
Version(value)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Timber.e(exception, "Version %s is null", value)
|
Timber.e(exception, "Invalid version - %s", value)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.core.featuretoggle.manager
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.tangem.core.featuretoggle.FeatureToggle
|
|
||||||
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||||
import com.tangem.core.featuretoggle.utils.associateToggles
|
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||||
import com.tangem.core.featuretoggle.version.VersionProvider
|
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||||
|
|
@ -42,7 +42,7 @@ internal class DevFeatureTogglesManager(
|
||||||
.toMutableMap()
|
.toMutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isFeatureEnabled(toggle: FeatureToggle): Boolean = featureTogglesMap.any { it.key == toggle.name }
|
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap.any { it.key == name }
|
||||||
|
|
||||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||||
|
|
||||||
|
|
@ -51,4 +51,9 @@ internal class DevFeatureTogglesManager(
|
||||||
featureTogglesMap[name] = isEnabled
|
featureTogglesMap[name] = isEnabled
|
||||||
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
|
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
||||||
|
featureTogglesMap = map
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package com.tangem.core.featuretoggle.manager
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
import com.tangem.core.featuretoggle.FeatureToggle
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for getting information about the availability of feature toggles
|
* Component for getting information about the availability of feature toggles
|
||||||
*
|
*
|
||||||
|
|
@ -12,6 +10,6 @@ interface FeatureTogglesManager {
|
||||||
/** Initialize manager */
|
/** Initialize manager */
|
||||||
suspend fun init()
|
suspend fun init()
|
||||||
|
|
||||||
/** Check feature toggle [toggle] availability */
|
/** Check feature toggle availability by name [name] */
|
||||||
fun isFeatureEnabled(toggle: FeatureToggle): Boolean
|
fun isFeatureEnabled(name: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.tangem.core.featuretoggle.manager
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
import com.tangem.core.featuretoggle.FeatureToggle
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||||
import com.tangem.core.featuretoggle.utils.associateToggles
|
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||||
import com.tangem.core.featuretoggle.version.VersionProvider
|
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||||
|
|
@ -25,5 +25,13 @@ internal class ProdFeatureTogglesManager(
|
||||||
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isFeatureEnabled(toggle: FeatureToggle): Boolean = featureToggles.any { it.key == toggle.name }
|
override fun isFeatureEnabled(name: String): Boolean = featureToggles.any { it.key == name }
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
fun getProdFeatureToggles() = featureToggles
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
fun setProdFeatureToggles(map: Map<String, Boolean>) {
|
||||||
|
featureToggles = map
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.core.featuretoggle.storage
|
package com.tangem.core.featuretoggle.storage
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
|
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
|
||||||
import com.tangem.datasource.asset.AssetReader
|
import com.tangem.datasource.asset.AssetReader
|
||||||
|
|
@ -29,6 +30,12 @@ internal class LocalFeatureTogglesStorage(
|
||||||
.onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") }
|
.onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
fun getConfigPath() = LOCAL_CONFIG_PATH
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||||
|
fun getFeatureToggles(): Iterable<FeatureToggle> = featureToggles
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
|
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.tangem.core.featuretoggle.contract
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package com.tangem.core.featuretoggle.contract
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.tangem.core.featuretoggle.storage.FeatureToggle
|
||||||
|
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||||
|
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||||
|
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||||
|
import com.tangem.datasource.local.AppPreferenceStorage
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerifyOrder
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verifyAll
|
||||||
|
import io.mockk.verifyOrder
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
internal class DevFeatureTogglesManagerTest {
|
||||||
|
|
||||||
|
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
|
||||||
|
private val appPreferenceStorage = mockk<AppPreferenceStorage>(relaxed = true)
|
||||||
|
private val jsonAdapter = mockk<JsonAdapter<Map<String, Boolean>>>()
|
||||||
|
private val versionProvider = mockk<VersionProvider>()
|
||||||
|
private val manager = DevFeatureTogglesManager(
|
||||||
|
localFeatureTogglesStorage = localFeatureTogglesStorage,
|
||||||
|
appPreferenceStorage = appPreferenceStorage,
|
||||||
|
jsonAdapter = jsonAdapter,
|
||||||
|
versionProvider = versionProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
|
||||||
|
val currentVersion = "1.0.0"
|
||||||
|
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
|
||||||
|
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
|
||||||
|
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||||
|
coEvery { versionProvider.get() } returns currentVersion
|
||||||
|
|
||||||
|
manager.init()
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
jsonAdapter.fromJson(savedFeatureToggles)
|
||||||
|
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 = "1.0.0"
|
||||||
|
val sharedPrefFeatureToggles = "[]"
|
||||||
|
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
coEvery { appPreferenceStorage.featureToggles } returns sharedPrefFeatureToggles
|
||||||
|
coEvery { jsonAdapter.fromJson(sharedPrefFeatureToggles) } returns savedFeatureTogglesMap
|
||||||
|
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||||
|
coEvery { versionProvider.get() } returns currentVersion
|
||||||
|
|
||||||
|
manager.init()
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
jsonAdapter.fromJson(sharedPrefFeatureToggles)
|
||||||
|
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 = "1.0.0"
|
||||||
|
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
coEvery { appPreferenceStorage.featureToggles } returns ""
|
||||||
|
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||||
|
coEvery { versionProvider.get() } returns currentVersion
|
||||||
|
|
||||||
|
manager.init()
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
versionProvider.get()
|
||||||
|
}
|
||||||
|
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||||
|
|
||||||
|
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 { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
|
||||||
|
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
|
||||||
|
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||||
|
coEvery { versionProvider.get() } returns null
|
||||||
|
|
||||||
|
manager.init()
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
jsonAdapter.fromJson(savedFeatureToggles)
|
||||||
|
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 false,
|
||||||
|
"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`() {
|
||||||
|
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
|
||||||
|
val resultMap = mutableMapOf(
|
||||||
|
changeableToggleName to false,
|
||||||
|
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.setFeatureToggles(resultMap)
|
||||||
|
coEvery { jsonAdapter.toJson(resultMap) } returns ""
|
||||||
|
|
||||||
|
manager.changeToggle(changeableToggleName, true)
|
||||||
|
|
||||||
|
resultMap[changeableToggleName] = true
|
||||||
|
verifyOrder { jsonAdapter.toJson(resultMap) }
|
||||||
|
|
||||||
|
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `change toggle that doesn't contains in map`() {
|
||||||
|
val resultMap = mutableMapOf(
|
||||||
|
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||||
|
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.setFeatureToggles(resultMap)
|
||||||
|
|
||||||
|
manager.changeToggle("FEATURE_TOGGLE", true)
|
||||||
|
|
||||||
|
verifyAll(inverse = true) { jsonAdapter.toJson(any()) }
|
||||||
|
|
||||||
|
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(
|
||||||
|
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||||
|
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.tangem.core.featuretoggle.storage.FeatureToggle
|
||||||
|
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||||
|
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||||
|
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerifyOrder
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verifyAll
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
internal class ProdFeatureTogglesManagerTest {
|
||||||
|
|
||||||
|
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
|
||||||
|
private val versionProvider = mockk<VersionProvider>()
|
||||||
|
private val manager = ProdFeatureTogglesManager(localFeatureTogglesStorage, versionProvider)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `successfully initialize storage`() = runTest {
|
||||||
|
val currentVersion = "1.0.0"
|
||||||
|
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||||
|
every { versionProvider.get() } returns currentVersion
|
||||||
|
|
||||||
|
manager.init()
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
versionProvider.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
val expected = localFeatureToggles.associateToggles(currentVersion)
|
||||||
|
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
|
||||||
|
every { versionProvider.get() } returns null
|
||||||
|
|
||||||
|
manager.init()
|
||||||
|
|
||||||
|
coVerifyOrder {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
versionProvider.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
every { localFeatureTogglesStorage.featureToggles } 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 { localFeatureTogglesStorage.init() }
|
||||||
|
verifyAll(inverse = true) { versionProvider.get() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||||
|
coEvery { localFeatureTogglesStorage.init() } just Runs
|
||||||
|
every { localFeatureTogglesStorage.featureToggles } 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 {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
versionProvider.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get feature availability if feature toggle exists`() {
|
||||||
|
val featureToggles = mapOf(
|
||||||
|
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||||
|
"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(
|
||||||
|
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||||
|
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val disabledFeatureToggles = mapOf(
|
||||||
|
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||||
|
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package com.tangem.core.featuretoggle.storage
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.tangem.datasource.asset.AssetReader
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verifyAll
|
||||||
|
import io.mockk.verifyOrder
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
internal class LocalFeatureTogglesStorageTest {
|
||||||
|
|
||||||
|
private val assetReader = mockk<AssetReader>()
|
||||||
|
private val jsonAdapter = mockk<JsonAdapter<List<FeatureToggle>>>()
|
||||||
|
private val storage = LocalFeatureTogglesStorage(assetReader, jsonAdapter)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `successfully initialize storage`() = runTest {
|
||||||
|
coEvery { assetReader.readJson(storage.getConfigPath()) } returns json
|
||||||
|
coEvery { jsonAdapter.fromJson(json) } returns featureToggles
|
||||||
|
|
||||||
|
storage.init()
|
||||||
|
|
||||||
|
verifyOrder {
|
||||||
|
assetReader.readJson(storage.getConfigPath())
|
||||||
|
jsonAdapter.fromJson(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
Truth.assertThat(storage.getFeatureToggles()).containsExactlyElementsIn(featureToggles)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `failure initialize storage if assetReader throws exception`() = runTest {
|
||||||
|
coEvery { assetReader.readJson(storage.getConfigPath()) } returns json
|
||||||
|
coEvery { jsonAdapter.fromJson(json) } throws IOException()
|
||||||
|
|
||||||
|
storage.init()
|
||||||
|
|
||||||
|
verifyOrder {
|
||||||
|
assetReader.readJson(storage.getConfigPath())
|
||||||
|
jsonAdapter.fromJson(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching { storage.getFeatureToggles() }
|
||||||
|
.onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") }
|
||||||
|
.onFailure {
|
||||||
|
Truth
|
||||||
|
.assertThat(it)
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains("Property featureToggles should be initialized before get.")
|
||||||
|
|
||||||
|
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
|
||||||
|
coEvery { assetReader.readJson(storage.getConfigPath()) } throws IOException()
|
||||||
|
|
||||||
|
storage.init()
|
||||||
|
|
||||||
|
verifyOrder { assetReader.readJson(storage.getConfigPath()) }
|
||||||
|
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||||
|
|
||||||
|
runCatching { storage.getFeatureToggles() }
|
||||||
|
.onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") }
|
||||||
|
.onFailure {
|
||||||
|
Truth
|
||||||
|
.assertThat(it)
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains("Property featureToggles should be initialized before get.")
|
||||||
|
|
||||||
|
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val json = """
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val featureToggles = listOf(
|
||||||
|
FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||||
|
FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -82,8 +82,9 @@ detekt = "1.21.0"
|
||||||
# region Testing
|
# region Testing
|
||||||
espresso = "3.4.0"
|
espresso = "3.4.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
truth = "1.1.3"
|
|
||||||
junitAndroidExt = "1.1.3"
|
junitAndroidExt = "1.1.3"
|
||||||
|
mockk = "1.13.4"
|
||||||
|
truth = "1.1.3"
|
||||||
# endregion Testing
|
# endregion Testing
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
@ -153,10 +154,12 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting",
|
||||||
# ndregion Detekt
|
# ndregion Detekt
|
||||||
|
|
||||||
# region Test
|
# region Test
|
||||||
|
test-coroutine = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutine" }
|
||||||
test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||||
test-junit = { module = "junit:junit", version.ref = "junit" }
|
test-junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" }
|
test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" }
|
||||||
test-truth = { module = "com.google.truth:truth", version.ref = "truth" }
|
test-truth = { module = "com.google.truth:truth", version.ref = "truth" }
|
||||||
|
test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||||
# endregion Test
|
# endregion Test
|
||||||
|
|
||||||
# region Other
|
# region Other
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue