diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts index f32b3b74cd..c5f9b07545 100644 --- a/core/featuretoggles/build.gradle.kts +++ b/core/featuretoggles/build.gradle.kts @@ -7,11 +7,16 @@ plugins { } dependencies { - implementation(deps.moshi) - implementation(deps.moshi.kotlin) + implementation(project(":core:datasource")) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) implementation(deps.timber) - implementation(project(":core:datasource")) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt deleted file mode 100644 index 6b2c82cc51..0000000000 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.core.featuretoggle - -/** - * Feature toggle - * -[REDACTED_AUTHOR] - */ -interface FeatureToggle { - - /** Feature toggle name */ - val name: String -} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt similarity index 76% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt index 6e5075c705..b506d80ec4 100644 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt @@ -1,5 +1,6 @@ package com.tangem.core.featuretoggle.contract +import androidx.annotation.VisibleForTesting import timber.log.Timber /** @@ -23,6 +24,11 @@ internal class Version private constructor(value: String) : Comparable 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) @@ -41,6 +47,15 @@ internal class Version private constructor(value: String) : Comparable 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 @@ -55,7 +70,7 @@ internal class Version private constructor(value: String) : Comparable return try { Version(value) } catch (exception: Exception) { - Timber.e(exception, "Version %s is null", value) + Timber.e(exception, "Invalid version - %s", value) return null } } diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt similarity index 87% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt index 81cf4b9a9d..5f01cdba6e 100644 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt @@ -1,7 +1,7 @@ package com.tangem.core.featuretoggle.manager +import androidx.annotation.VisibleForTesting import com.squareup.moshi.JsonAdapter -import com.tangem.core.featuretoggle.FeatureToggle import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage import com.tangem.core.featuretoggle.utils.associateToggles import com.tangem.core.featuretoggle.version.VersionProvider @@ -42,7 +42,7 @@ internal class DevFeatureTogglesManager( .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 = featureTogglesMap @@ -51,4 +51,9 @@ internal class DevFeatureTogglesManager( featureTogglesMap[name] = isEnabled appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap) } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun setFeatureToggles(map: MutableMap) { + featureTogglesMap = map + } } \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt similarity index 60% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt index 37cb34b343..6111e7f73b 100644 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt @@ -1,7 +1,5 @@ package com.tangem.core.featuretoggle.manager -import com.tangem.core.featuretoggle.FeatureToggle - /** * Component for getting information about the availability of feature toggles * @@ -12,6 +10,6 @@ interface FeatureTogglesManager { /** Initialize manager */ suspend fun init() - /** Check feature toggle [toggle] availability */ - fun isFeatureEnabled(toggle: FeatureToggle): Boolean + /** Check feature toggle availability by name [name] */ + fun isFeatureEnabled(name: String): Boolean } \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt similarity index 70% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt index bbfe8641d2..be8a4046d2 100644 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt @@ -1,6 +1,6 @@ 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.utils.associateToggles import com.tangem.core.featuretoggle.version.VersionProvider @@ -25,5 +25,13 @@ internal class ProdFeatureTogglesManager( .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) { + featureToggles = map + } } \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt similarity index 81% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt index 8cd19f39d4..befc11c3b0 100644 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt @@ -1,5 +1,6 @@ package com.tangem.core.featuretoggle.storage +import androidx.annotation.VisibleForTesting import com.squareup.moshi.JsonAdapter import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH 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") } } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getConfigPath() = LOCAL_CONFIG_PATH + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun getFeatureToggles(): Iterable = featureToggles + private companion object { const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config" } diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt similarity index 100% rename from core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt rename to core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt new file mode 100644 index 0000000000..476c967367 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt @@ -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() + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt new file mode 100644 index 0000000000..f8763c6a00 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt @@ -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 + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt new file mode 100644 index 0000000000..06ada09a84 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt @@ -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() + private val appPreferenceStorage = mockk(relaxed = true) + private val jsonAdapter = mockk>>() + private val versionProvider = mockk() + 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()) } + + val expected = localFeatureToggles + .associateToggles(currentVersion) + .mapValues(Map.Entry::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"), + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt new file mode 100644 index 0000000000..06c5023987 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt @@ -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() + private val versionProvider = mockk() + 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, + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt new file mode 100644 index 0000000000..fae88c79f8 --- /dev/null +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt @@ -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() + private val jsonAdapter = mockk>>() + 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()) } + + 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"), + ) + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d6186e0348..8388640d72 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,8 +82,9 @@ detekt = "1.21.0" # region Testing espresso = "3.4.0" junit = "4.13.2" -truth = "1.1.3" junitAndroidExt = "1.1.3" +mockk = "1.13.4" +truth = "1.1.3" # endregion Testing [plugins] @@ -153,10 +154,12 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", # ndregion Detekt # 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-junit = { module = "junit:junit", version.ref = "junit" } test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" } test-truth = { module = "com.google.truth:truth", version.ref = "truth" } +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } # endregion Test # region Other