diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt index e41aee1ef2..0301a65b80 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -9,6 +9,7 @@ import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement import timber.log.Timber +import java.lang.reflect.Field class ApplicationInjectionExecutionRule( private val toggleStates: Map, @@ -17,7 +18,7 @@ class ApplicationInjectionExecutionRule( private val tangemApplication: TangemApplication get() = ApplicationProvider.getApplicationContext() - private var originalFeatureTogglesValues: Map? = null + private var originalVersionValues: Map? = null override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @@ -41,10 +42,9 @@ class ApplicationInjectionExecutionRule( } } - @Suppress("UNCHECKED_CAST") private fun saveOriginalFeatureToggles() { try { - originalFeatureTogglesValues = FeatureToggles.values as Map + originalVersionValues = FeatureToggles.entries.associateWith { it.version } } catch (e: Exception) { Timber.e("Failed to save original toggles values: ${e.message}") } @@ -52,22 +52,14 @@ class ApplicationInjectionExecutionRule( private fun overrideFeatureToggles() { try { - val originalValues = originalFeatureTogglesValues ?: FeatureToggles.values - val newValues = originalValues.toMutableMap() + val versionField = getVersionField() - toggleStates.forEach { (toggle, enabled) -> - if (enabled) { - newValues[toggle] = "1.0.0" - } else { - newValues.remove(toggle) - } + FeatureToggles.entries.forEach { toggle -> + val enabled = toggleStates[toggle.rawName] ?: return@forEach + val newVersion = if (enabled) ENABLED_VERSION else DISABLED_VERSION + versionField.set(toggle, newVersion) } - val companionClass = FeatureToggles.Companion::class.java - val valuesField = companionClass.getDeclaredField("values") - valuesField.isAccessible = true - valuesField.set(FeatureToggles.Companion, newValues) - Timber.i("FeatureToggles.values updated: $toggleStates") } catch (e: Exception) { Timber.e("FeatureToggles.values didn't change with error: ${e.message}") @@ -76,15 +68,27 @@ class ApplicationInjectionExecutionRule( private fun restoreOriginalFeatureToggles() { try { - if (originalFeatureTogglesValues != null) { - val companionClass = FeatureToggles.Companion::class.java - val valuesField = companionClass.getDeclaredField("values") - valuesField.isAccessible = true - valuesField.set(FeatureToggles.Companion, originalFeatureTogglesValues) - Timber.i("FeatureToggles.values restored") + val saved = originalVersionValues ?: return + val versionField = getVersionField() + + saved.forEach { (toggle, originalVersion) -> + versionField.set(toggle, originalVersion) } + + Timber.i("FeatureToggles.values restored") } catch (e: Exception) { Timber.e("FeatureToggles.values didn't restored with error: ${e.message}") } } + + private fun getVersionField(): Field { + val field = FeatureToggles::class.java.getDeclaredField("version") + field.isAccessible = true + return field + } + + private companion object { + const val ENABLED_VERSION = "1.0.0" + const val DISABLED_VERSION = "undefined" + } } \ No newline at end of file diff --git a/core/config-toggles/detekt-baseline-debug.xml b/core/config-toggles/detekt-baseline-debug.xml deleted file mode 100644 index 274a75d8d4..0000000000 --- a/core/config-toggles/detekt-baseline-debug.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - Indentation:ExcludedBlockchainToggles.kt$ExcludedBlockchainToggles$ - Indentation:FeatureToggles.kt$FeatureToggles$ - - diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt index 98311b8d7a..b40b0d3b48 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManager.kt @@ -1,7 +1,7 @@ package com.tangem.core.configtoggle.blockchain.impl -import com.tangem.core.configtoggle.ExcludedBlockchainToggles import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.utils.defineTogglesAvailability import com.tangem.core.configtoggle.utils.toTableString @@ -12,11 +12,13 @@ import kotlin.properties.Delegates /** * [MutableExcludedBlockchainsManager] implementation in dev or mocked build * - * @property versionProvider application version provider - * @property localTogglesStorage local storage for blockchain toggles + * @property versionProvider application version provider + * @property excludedBlockchainTogglesProvider provider for excluded blockchain toggle entries + * @property localTogglesStorage local storage for blockchain toggles */ internal class DevExcludedBlockchainsManager( private val versionProvider: VersionProvider, + private val excludedBlockchainTogglesProvider: ExcludedBlockchainTogglesProvider, private val localTogglesStorage: LocalTogglesStorage, ) : MutableExcludedBlockchainsManager { @@ -63,6 +65,7 @@ internal class DevExcludedBlockchainsManager( private fun getFileBlockchainToggles(): Map { val appVersion = versionProvider.get() - return ExcludedBlockchainToggles.values.defineTogglesAvailability(appVersion = appVersion) + return excludedBlockchainTogglesProvider.getToggles() + .defineTogglesAvailability(appVersion = appVersion) } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt index bc9eb689c4..bf5f979fc0 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManager.kt @@ -1,17 +1,19 @@ package com.tangem.core.configtoggle.blockchain.impl -import com.tangem.core.configtoggle.ExcludedBlockchainToggles import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider import com.tangem.core.configtoggle.utils.defineTogglesAvailability import com.tangem.core.configtoggle.version.VersionProvider /** * [ExcludedBlockchainsManager] implementation in PROD build * - * @property versionProvider application version provider + * @property versionProvider application version provider + * @property excludedBlockchainTogglesProvider provider for excluded blockchain toggle entries */ internal class ProdExcludedBlockchainsManager( private val versionProvider: VersionProvider, + private val excludedBlockchainTogglesProvider: ExcludedBlockchainTogglesProvider, ) : ExcludedBlockchainsManager { override val excludedBlockchainsIds: Set = getBlockchainToggles() @@ -19,7 +21,7 @@ internal class ProdExcludedBlockchainsManager( private fun getBlockchainToggles(): Set { val appVersion = versionProvider.get() - return ExcludedBlockchainToggles.values + return excludedBlockchainTogglesProvider.getToggles() .defineTogglesAvailability(appVersion = appVersion) .filterValues { !it } .keys diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/provider/DefaultExcludedBlockchainTogglesProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/provider/DefaultExcludedBlockchainTogglesProvider.kt new file mode 100644 index 0000000000..9f855470fa --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/provider/DefaultExcludedBlockchainTogglesProvider.kt @@ -0,0 +1,15 @@ +package com.tangem.core.configtoggle.blockchain.provider + +import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import javax.inject.Inject + +/** + * Default implementation of [ExcludedBlockchainTogglesProvider] that reads from the generated + * [ExcludedBlockchainToggles] enum. + */ +internal class DefaultExcludedBlockchainTogglesProvider @Inject constructor() : ExcludedBlockchainTogglesProvider { + + override fun getToggles(): Map { + return ExcludedBlockchainToggles.entries.associate { it.rawName to it.version } + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/provider/ExcludedBlockchainTogglesProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/provider/ExcludedBlockchainTogglesProvider.kt new file mode 100644 index 0000000000..6dea0f5acf --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/provider/ExcludedBlockchainTogglesProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.core.configtoggle.blockchain.provider + +/** + * Provider for excluded blockchain toggle entries. + * This interface allows for easier testing by enabling mock implementations. + */ +internal interface ExcludedBlockchainTogglesProvider { + + /** + * Returns a map of blockchain IDs to their version strings. + * The version string is "undefined" for disabled toggles, or a version number like "5.21.0" for enabled ones. + */ + fun getToggles(): Map +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt index 14d9514eae..2e00b13cfa 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt @@ -5,6 +5,7 @@ import com.tangem.core.configtoggle.BuildConfig import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.blockchain.impl.DevExcludedBlockchainsManager import com.tangem.core.configtoggle.blockchain.impl.ProdExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.provider.DefaultExcludedBlockchainTogglesProvider import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.DefaultVersionProvider import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -26,17 +27,22 @@ internal object ExcludedBlockchainsManagerModule { appPreferencesStore: AppPreferencesStore, ): ExcludedBlockchainsManager { val versionProvider = DefaultVersionProvider(context) + val excludedBlockchainTogglesProvider = DefaultExcludedBlockchainTogglesProvider() return if (BuildConfig.TESTER_MENU_ENABLED) { DevExcludedBlockchainsManager( versionProvider = versionProvider, + excludedBlockchainTogglesProvider = excludedBlockchainTogglesProvider, localTogglesStorage = LocalTogglesStorage( appPreferencesStore = appPreferencesStore, preferencesKey = LocalTogglesStorage.EXCLUDED_BLOCKCHAINS_KEY, ), ) } else { - ProdExcludedBlockchainsManager(versionProvider = versionProvider) + ProdExcludedBlockchainsManager( + versionProvider = versionProvider, + excludedBlockchainTogglesProvider = excludedBlockchainTogglesProvider, + ) } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index 877f25842d..67c5f02283 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -5,6 +5,7 @@ import com.tangem.core.configtoggle.BuildConfig import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager +import com.tangem.core.configtoggle.feature.provider.DefaultFeatureTogglesProvider import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.DefaultVersionProvider import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -26,17 +27,22 @@ internal object FeatureTogglesManagerModule { appPreferencesStore: AppPreferencesStore, ): FeatureTogglesManager { val versionProvider = DefaultVersionProvider(context) + val featureTogglesProvider = DefaultFeatureTogglesProvider() return if (BuildConfig.TESTER_MENU_ENABLED) { DevFeatureTogglesManager( versionProvider = versionProvider, + featureTogglesProvider = featureTogglesProvider, featureTogglesLocalStorage = LocalTogglesStorage( appPreferencesStore = appPreferencesStore, preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY, ), ) } else { - ProdFeatureTogglesManager(versionProvider = versionProvider) + ProdFeatureTogglesManager( + versionProvider = versionProvider, + featureTogglesProvider = featureTogglesProvider, + ) } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index d59bac9d71..70f85b6511 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -1,8 +1,8 @@ package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting -import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager +import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.utils.defineTogglesAvailability import com.tangem.core.configtoggle.utils.toTableString @@ -14,10 +14,12 @@ import kotlin.properties.Delegates * Feature toggles manager implementation in dev or mocked build * * @property versionProvider application version provider + * @property featureTogglesProvider provider for feature toggle entries * @property featureTogglesLocalStorage local storage for feature toggles */ internal class DevFeatureTogglesManager( private val versionProvider: VersionProvider, + private val featureTogglesProvider: FeatureTogglesProvider, private val featureTogglesLocalStorage: LocalTogglesStorage, ) : MutableFeatureTogglesManager { @@ -60,7 +62,8 @@ internal class DevFeatureTogglesManager( private fun getFileFeatureToggles(): Map { val appVersion = versionProvider.get() - return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion) + return featureTogglesProvider.getToggles() + .defineTogglesAvailability(appVersion = appVersion) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index 261ba22e8e..bb2ab6e619 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -1,18 +1,20 @@ package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting -import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider import com.tangem.core.configtoggle.utils.defineTogglesAvailability import com.tangem.core.configtoggle.version.VersionProvider /** * Feature toggles manager implementation in PROD build * - * @property versionProvider application version provider + * @property versionProvider application version provider + * @property featureTogglesProvider provider for feature toggle entries */ internal class ProdFeatureTogglesManager( private val versionProvider: VersionProvider, + private val featureTogglesProvider: FeatureTogglesProvider, ) : FeatureTogglesManager { private val featureToggles: Map = getFileFeatureToggles() @@ -22,7 +24,8 @@ internal class ProdFeatureTogglesManager( private fun getFileFeatureToggles(): Map { val appVersion = versionProvider.get() - return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion) + return featureTogglesProvider.getToggles() + .defineTogglesAvailability(appVersion = appVersion) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/provider/DefaultFeatureTogglesProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/provider/DefaultFeatureTogglesProvider.kt new file mode 100644 index 0000000000..03258d5203 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/provider/DefaultFeatureTogglesProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.core.configtoggle.feature.provider + +import com.tangem.core.configtoggle.FeatureToggles +import javax.inject.Inject + +/** + * Default implementation of [FeatureTogglesProvider] that reads from the generated [FeatureToggles] enum. + */ +internal class DefaultFeatureTogglesProvider @Inject constructor() : FeatureTogglesProvider { + + override fun getToggles(): Map { + return FeatureToggles.entries.associate { it.rawName to it.version } + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/provider/FeatureTogglesProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/provider/FeatureTogglesProvider.kt new file mode 100644 index 0000000000..3d423d1b88 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/provider/FeatureTogglesProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.core.configtoggle.feature.provider + +/** + * Provider for feature toggle entries. + * This interface allows for easier testing by enabling mock implementations. + */ +internal interface FeatureTogglesProvider { + + /** + * Returns a map of toggle names to their version strings. + * The version string is "undefined" for disabled toggles, or a version number like "1.0.0" for enabled ones. + */ + fun getToggles(): Map +} \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt index 18eb584b52..0b48b1f6b1 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/DevExcludedBlockchainsManagerTest.kt @@ -1,7 +1,7 @@ package com.tangem.core.configtoggle.blockchain.impl import com.google.common.truth.Truth -import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.VersionProvider import io.mockk.* @@ -12,42 +12,74 @@ import org.junit.jupiter.api.* internal class DevExcludedBlockchainsManagerTest { private val versionProvider = mockk() + private val excludedBlockchainTogglesProvider = mockk() private val localTogglesStorage = mockk(relaxUnitFun = true) + private val testVersion = "5.30" + + private val testToggles = mapOf( + "ENABLED_BLOCKCHAIN" to "5.21.0", + "DISABLED_BLOCKCHAIN" to "undefined", + "FUTURE_BLOCKCHAIN" to "6.0.0", + ) + + private fun getExpectedFileToggles(appVersion: String?): Map { + if (appVersion.isNullOrEmpty()) { + return testToggles.mapValues { false } + } + return testToggles.mapValues { (_, version) -> + version != "undefined" && isVersionSufficient(appVersion, version) + } + } + + private fun getExcludedBlockchains(appVersion: String?): Set { + return getExpectedFileToggles(appVersion).filterValues { !it }.keys + } + + private fun isVersionSufficient(appVersion: String, requiredVersion: String): Boolean { + val appParts = appVersion.split(".").mapNotNull { it.toIntOrNull() } + val reqParts = requiredVersion.split(".").mapNotNull { it.toIntOrNull() } + for (i in 0 until maxOf(appParts.size, reqParts.size)) { + val app = appParts.getOrElse(i) { 0 } + val req = reqParts.getOrElse(i) { 0 } + if (app > req) return true + if (app < req) return false + } + return true + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Initialization { - @BeforeAll - fun setupAll() { - val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0") - mockkObject(ExcludedBlockchainToggles) - every { ExcludedBlockchainToggles.values } returns toggles - } - - @AfterAll - fun tearDownAll() { - unmockkObject(ExcludedBlockchainToggles) + @BeforeEach + fun setupEach() { + every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles } @AfterEach fun tearDownEach() { - clearMocks(versionProvider, localTogglesStorage) + clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage) } @Test fun `successfully initialize manager`() = runTest { // Arrange - val appVersion = "1.0.0" - val savedToggles = mapOf("CHAIN_1" to false, "CHAIN_2" to true) + val appVersion = testVersion + val fileToggles = getExpectedFileToggles(appVersion) + val savedToggles = fileToggles.mapValues { !it.value } every { versionProvider.get() } returns appVersion coEvery { localTogglesStorage.getSyncOrEmpty() } returns savedToggles // Act - val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds + val actual = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ).excludedBlockchainsIds // Assert - val expected = setOf("CHAIN_1") + val expected = savedToggles.filterValues { !it }.keys Truth.assertThat(actual).containsExactlyElementsIn(expected) coVerifyOrder { @@ -59,14 +91,20 @@ internal class DevExcludedBlockchainsManagerTest { @Test fun `successfully initialize manager if versionProvider returns null`() = runTest { // Arrange - every { versionProvider.get() } returns null + val appVersion: String? = null + every { versionProvider.get() } returns appVersion coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() // Act - val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds + val actual = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ).excludedBlockchainsIds // Assert - Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2") + val expected = getExcludedBlockchains(appVersion) + Truth.assertThat(actual).containsExactlyElementsIn(expected) coVerifyOrder { versionProvider.get() @@ -77,14 +115,20 @@ internal class DevExcludedBlockchainsManagerTest { @Test fun `successfully initialize manager if storage returns empty map`() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" + val appVersion = testVersion + every { versionProvider.get() } returns appVersion coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() // Act - val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds + val actual = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ).excludedBlockchainsIds // Assert - Truth.assertThat(actual).containsExactly("CHAIN_2") + val expected = getExcludedBlockchains(appVersion) + Truth.assertThat(actual).containsExactlyElementsIn(expected) coVerifyOrder { versionProvider.get() @@ -95,13 +139,19 @@ internal class DevExcludedBlockchainsManagerTest { @Test fun `failure initialize manager if storage throws exception`() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" + val appVersion = testVersion val exception = Exception("Test exception") + every { versionProvider.get() } returns appVersion coEvery { localTogglesStorage.getSyncOrEmpty() } throws exception // Act - val actual = runCatching { DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) } - .exceptionOrNull()!! + val actual = runCatching { + DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ) + }.exceptionOrNull()!! // Assert Truth.assertThat(actual).isInstanceOf(exception::class.java) @@ -118,21 +168,34 @@ internal class DevExcludedBlockchainsManagerTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class ExcludeBlockchain { + @BeforeEach + fun setupEach() { + every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage) + } + @Test fun excludeBlockchain_changesStatusAndSaves() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" + every { versionProvider.get() } returns testVersion coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() - - val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) - manager.excludeBlockchain("CHAIN_1", false) + val manager = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ) // Act + manager.excludeBlockchain("ENABLED_BLOCKCHAIN", false) val actual = manager.excludedBlockchainsIds // Assert - Truth.assertThat(actual).contains("CHAIN_1") - coVerify { localTogglesStorage.store(match { it["CHAIN_1"] == false }) } + Truth.assertThat(actual).contains("ENABLED_BLOCKCHAIN") + coVerify { localTogglesStorage.store(match { it["ENABLED_BLOCKCHAIN"] == false }) } } } @@ -140,13 +203,26 @@ internal class DevExcludedBlockchainsManagerTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class IsMatchLocalConfig { + @BeforeEach + fun setupEach() { + every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage) + } + @Test fun isMatchLocalConfig_returnsTrueIfMatchesFile() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" + every { versionProvider.get() } returns testVersion coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() - - val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) + val manager = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ) // Act val actual = manager.isMatchLocalConfig() @@ -158,13 +234,17 @@ internal class DevExcludedBlockchainsManagerTest { @Test fun isMatchLocalConfig_returnsFalseIfDiffersFromFile() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" + every { versionProvider.get() } returns testVersion + val fileToggles = getExpectedFileToggles(testVersion) coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() - - val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) - manager.excludeBlockchain("CHAIN_1", false) + val manager = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ) // Act + manager.excludeBlockchain("ENABLED_BLOCKCHAIN", !fileToggles.getValue("ENABLED_BLOCKCHAIN")) val actual = manager.isMatchLocalConfig() // Assert @@ -176,28 +256,36 @@ internal class DevExcludedBlockchainsManagerTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class RecoverLocalConfig { + @BeforeEach + fun setupEach() { + every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, excludedBlockchainTogglesProvider, localTogglesStorage) + } + @Test fun recoverLocalConfig_resetsToFileAndSaves() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" - - val toggles = mapOf("CHAIN_1" to "2.0.0", "CHAIN_2" to "2.0.0") - mockkObject(ExcludedBlockchainToggles) - every { ExcludedBlockchainToggles.values } returns toggles - - coEvery { localTogglesStorage.getSyncOrEmpty() } returns mapOf("CHAIN_1" to true) - - val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) - manager.recoverLocalConfig() + val appVersion = testVersion + every { versionProvider.get() } returns appVersion + coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap() + val manager = DevExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + localTogglesStorage, + ) + manager.excludeBlockchain("ENABLED_BLOCKCHAIN", false) // Act + manager.recoverLocalConfig() val actual = manager.excludedBlockchainsIds // Assert - Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2") - - unmockkObject(ExcludedBlockchainToggles) - clearMocks(versionProvider, localTogglesStorage) + val expected = getExcludedBlockchains(appVersion) + Truth.assertThat(actual).containsExactlyElementsIn(expected) } } } \ No newline at end of file diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt index 68fdf3c8b6..29905af0d3 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/blockchain/impl/ProdExcludedBlockchainsManagerTest.kt @@ -1,9 +1,12 @@ package com.tangem.core.configtoggle.blockchain.impl import com.google.common.truth.Truth -import com.tangem.core.configtoggle.ExcludedBlockchainToggles +import com.tangem.core.configtoggle.blockchain.provider.ExcludedBlockchainTogglesProvider import com.tangem.core.configtoggle.version.VersionProvider -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.* @@ -11,39 +14,65 @@ import org.junit.jupiter.api.* internal class ProdExcludedBlockchainsManagerTest { private val versionProvider = mockk() + private val excludedBlockchainTogglesProvider = mockk() + + private val testVersion = "5.30" + + private val testToggles = mapOf( + "ENABLED_BLOCKCHAIN" to "5.21.0", + "DISABLED_BLOCKCHAIN" to "undefined", + "FUTURE_BLOCKCHAIN" to "6.0.0", + ) + + private fun getExcludedBlockchains(appVersion: String?): Set { + if (appVersion.isNullOrEmpty()) { + return testToggles.keys + } + return testToggles.filterValues { version -> + version == "undefined" || !isVersionSufficient(appVersion, version) + }.keys + } + + private fun isVersionSufficient(appVersion: String, requiredVersion: String): Boolean { + val appParts = appVersion.split(".").mapNotNull { it.toIntOrNull() } + val reqParts = requiredVersion.split(".").mapNotNull { it.toIntOrNull() } + for (i in 0 until maxOf(appParts.size, reqParts.size)) { + val app = appParts.getOrElse(i) { 0 } + val req = reqParts.getOrElse(i) { 0 } + if (app > req) return true + if (app < req) return false + } + return true + } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Initialization { - @BeforeAll - fun setupAll() { - val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0") - mockkObject(ExcludedBlockchainToggles) - every { ExcludedBlockchainToggles.values } returns toggles - } - - @AfterAll - fun tearDownAll() { - unmockkObject(ExcludedBlockchainToggles) + @BeforeEach + fun setupEach() { + every { excludedBlockchainTogglesProvider.getToggles() } returns testToggles } @AfterEach fun tearDownEach() { - clearMocks(versionProvider) + clearMocks(versionProvider, excludedBlockchainTogglesProvider) } @Test fun `successfully initialize excluded blockchains`() = runTest { // Arrange - val appVersion = "1.0.0" + val appVersion = testVersion every { versionProvider.get() } returns appVersion // Act - val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds + val actual = ProdExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + ).excludedBlockchainsIds // Assert - val expected = setOf("CHAIN_2") + val expected = getExcludedBlockchains(appVersion) Truth.assertThat(actual).containsExactlyElementsIn(expected) coVerifyOrder { versionProvider.get() } @@ -52,13 +81,17 @@ internal class ProdExcludedBlockchainsManagerTest { @Test fun `all blockchains excluded if versionProvider returns null`() = runTest { // Arrange - every { versionProvider.get() } returns null + val appVersion: String? = null + every { versionProvider.get() } returns appVersion // Act - val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds + val actual = ProdExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + ).excludedBlockchainsIds // Assert - val expected = setOf("CHAIN_1", "CHAIN_2") + val expected = getExcludedBlockchains(appVersion) Truth.assertThat(actual).containsExactlyElementsIn(expected) coVerifyOrder { versionProvider.get() } @@ -67,13 +100,17 @@ internal class ProdExcludedBlockchainsManagerTest { @Test fun `all blockchains excluded if versionProvider returns empty string`() = runTest { // Arrange - every { versionProvider.get() } returns "" + val appVersion = "" + every { versionProvider.get() } returns appVersion // Act - val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds + val actual = ProdExcludedBlockchainsManager( + versionProvider, + excludedBlockchainTogglesProvider, + ).excludedBlockchainsIds // Assert - val expected = setOf("CHAIN_1", "CHAIN_2") + val expected = getExcludedBlockchains(appVersion) Truth.assertThat(actual).containsExactlyElementsIn(expected) coVerifyOrder { versionProvider.get() } @@ -86,7 +123,9 @@ internal class ProdExcludedBlockchainsManagerTest { every { versionProvider.get() } throws exception // Act - val actual = runCatching { ProdExcludedBlockchainsManager(versionProvider) }.exceptionOrNull()!! + val actual = runCatching { + ProdExcludedBlockchainsManager(versionProvider, excludedBlockchainTogglesProvider) + }.exceptionOrNull()!! // Assert Truth.assertThat(actual).isInstanceOf(exception::class.java) diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt index 8e426130a0..d8ccb683e7 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevFeatureTogglesManagerTest.kt @@ -1,9 +1,8 @@ package com.tangem.core.configtoggle.manager import com.google.common.truth.Truth -import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager -import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel +import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider import com.tangem.core.configtoggle.storage.LocalTogglesStorage import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.test.core.ProvideTestModels @@ -12,51 +11,55 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.* import org.junit.jupiter.params.ParameterizedTest -/** -[REDACTED_AUTHOR] - */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DevFeatureTogglesManagerTest { private val versionProvider = mockk() + private val featureTogglesProvider = mockk() private val featureTogglesLocalStorage = mockk(relaxUnitFun = true) + private val testToggles = mapOf( + "ENABLED_TOGGLE" to "1.0.0", + "DISABLED_TOGGLE" to "undefined", + ) + + private fun getExpectedFileToggles(appVersion: String?): Map = + testToggles.mapValues { (_, version) -> + version != "undefined" && !appVersion.isNullOrEmpty() + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Initialization { - @BeforeAll - fun setupAll() { - val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0") - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns featureToggles - } - - @AfterAll - fun tearDownAll() { - unmockkObject(FeatureToggles) + @BeforeEach + fun setupEach() { + every { featureTogglesProvider.getToggles() } returns testToggles } @AfterEach fun tearDownEach() { - clearMocks(versionProvider, featureTogglesLocalStorage) + clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) } @Test fun `successfully initialize manager`() = runTest { // Arrange val appVersion = "1.0.0" - val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true) - + val fileToggles = getExpectedFileToggles(appVersion) + val savedToggles = fileToggles.mapValues { !it.value } every { versionProvider.get() } returns appVersion - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles // Act - val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + val actual = DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ).getFeatureToggles() // Assert - val expected = savedFeatureToggles + val expected = savedToggles Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { @@ -68,17 +71,21 @@ internal class DevFeatureTogglesManagerTest { @Test fun `successfully initialize manager if versionProvider returns null`() = runTest { // Arrange - val appVersion = null - val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true) - + val appVersion: String? = null + val fileToggles = getExpectedFileToggles(appVersion) + val savedToggles = fileToggles.mapValues { !it.value } every { versionProvider.get() } returns appVersion - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles // Act - val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + val actual = DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ).getFeatureToggles() // Assert - val expected = savedFeatureToggles + val expected = savedToggles Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { @@ -91,16 +98,20 @@ internal class DevFeatureTogglesManagerTest { fun `successfully initialize manager if versionProvider returns empty string`() = runTest { // Arrange val appVersion = "" - val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true) - + val fileToggles = getExpectedFileToggles(appVersion) + val savedToggles = fileToggles.mapValues { !it.value } every { versionProvider.get() } returns appVersion - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles // Act - val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + val actual = DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ).getFeatureToggles() // Assert - val expected = savedFeatureToggles + val expected = savedToggles Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { @@ -116,8 +127,13 @@ internal class DevFeatureTogglesManagerTest { every { versionProvider.get() } throws exception // Act - val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) } - .exceptionOrNull()!! + val actual = runCatching { + DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ) + }.exceptionOrNull()!! // Assert Truth.assertThat(actual).isInstanceOf(exception::class.java) @@ -131,16 +147,18 @@ internal class DevFeatureTogglesManagerTest { fun `successfully initialize manager if storage returns empty map`() = runTest { // Arrange val appVersion = "1.0.0" - val savedFeatureToggles = emptyMap() - every { versionProvider.get() } returns appVersion - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap() // Act - val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + val actual = DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ).getFeatureToggles() // Assert - val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false) + val expected = getExpectedFileToggles(appVersion) Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { @@ -153,16 +171,20 @@ internal class DevFeatureTogglesManagerTest { fun `successfully initialize manager if storage returns unknown toggles`() = runTest { // Arrange val appVersion = "1.0.0" - val savedFeatureToggles = mapOf("TOGGLE_3" to true) - + val fileToggles = getExpectedFileToggles(appVersion) + val unknownSavedToggles = fileToggles + ("UNKNOWN_TOGGLE" to true) every { versionProvider.get() } returns appVersion - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns unknownSavedToggles // Act - val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles() + val actual = DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ).getFeatureToggles() // Assert - val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false) + val expected = fileToggles Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { @@ -176,13 +198,17 @@ internal class DevFeatureTogglesManagerTest { // Arrange val appVersion = "1.0.0" val exception = Exception("Test exception") - every { versionProvider.get() } returns appVersion coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } throws exception // Act - val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) } - .exceptionOrNull()!! + val actual = runCatching { + DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ) + }.exceptionOrNull()!! // Assert Truth.assertThat(actual).isInstanceOf(exception::class.java) @@ -203,29 +229,19 @@ internal class DevFeatureTogglesManagerTest { @BeforeAll fun setupAll() { + every { featureTogglesProvider.getToggles() } returns testToggles every { versionProvider.get() } returns "1.0.0" - - val featureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", - "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + val savedToggles = mapOf( + "DISABLED_TOGGLE" to true, + "ENABLED_TOGGLE" to false, ) - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns featureToggles - - val savedFeatureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to true, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles - - manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles + manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) } @AfterAll fun tearDownAll() { - clearMocks(versionProvider, featureTogglesLocalStorage) - unmockkObject(FeatureToggles) + clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) } @ParameterizedTest @@ -235,33 +251,42 @@ internal class DevFeatureTogglesManagerTest { val actual = manager.isFeatureEnabled(name = model.name) // Assert - val expected = model.expected - Truth.assertThat(actual).isEqualTo(expected) + Truth.assertThat(actual).isEqualTo(model.expected) } private fun provideTestModels() = listOf( - IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = false), - IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = true), - IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false), - IsFeatureEnabledModel(name = "", expected = false), + IsFeatureEnabledModel(name = "ENABLED_TOGGLE", expected = false), + IsFeatureEnabledModel(name = "DISABLED_TOGGLE", expected = true), ) } + data class IsFeatureEnabledModel(val name: String, val expected: Boolean) + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class IsMatchLocalConfig { + @BeforeEach + fun setupEach() { + every { featureTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) + } + @ParameterizedTest @ProvideTestModels fun isMatchLocalConfig(model: IsMatchLocalConfigModel) = runTest { // Arrange every { versionProvider.get() } returns "1.0.0" - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns model.fileFeatureToggles coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap() - - val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).apply { + val manager = DevFeatureTogglesManager( + versionProvider, + featureTogglesProvider, + featureTogglesLocalStorage, + ).apply { setFeatureToggles(model.storedFeatureToggles.toMutableMap()) } @@ -269,54 +294,45 @@ internal class DevFeatureTogglesManagerTest { val actual = manager.isMatchLocalConfig() // Assert - val expected = model.expected - Truth.assertThat(actual).isEqualTo(expected) + Truth.assertThat(actual).isEqualTo(model.expected) coVerifyOrder { versionProvider.get() featureTogglesLocalStorage.getSyncOrEmpty() } - - clearMocks(versionProvider, featureTogglesLocalStorage) - unmockkObject(FeatureToggles) } - private fun provideTestModels() = listOf( - IsMatchLocalConfigModel( - fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), - storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false), - expected = true, - ), - IsMatchLocalConfigModel( - fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), - storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true), - expected = false, - ), - IsMatchLocalConfigModel( - fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), - storedFeatureToggles = mapOf("TOGGLE_1" to true), - expected = false, - ), - IsMatchLocalConfigModel( - fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"), - storedFeatureToggles = mapOf("TOGGLE_3" to true, "TOGGLE_4" to false), - expected = false, - ), - IsMatchLocalConfigModel( - fileFeatureToggles = emptyMap(), - storedFeatureToggles = emptyMap(), - expected = true, - ), - IsMatchLocalConfigModel( - fileFeatureToggles = emptyMap(), - storedFeatureToggles = mapOf("TOGGLE_1" to true), - expected = false, - ), - ) + private fun provideTestModels(): List { + val appVersion = "1.0.0" + val fileToggles = getExpectedFileToggles(appVersion) + val firstToggleName = "ENABLED_TOGGLE" + + return listOf( + IsMatchLocalConfigModel( + storedFeatureToggles = fileToggles, + expected = true, + ), + IsMatchLocalConfigModel( + storedFeatureToggles = fileToggles.mapValues { !it.value }, + expected = false, + ), + IsMatchLocalConfigModel( + storedFeatureToggles = mapOf(firstToggleName to fileToggles.getValue(firstToggleName)), + expected = false, + ), + IsMatchLocalConfigModel( + storedFeatureToggles = mapOf("UNKNOWN_TOGGLE" to true), + expected = false, + ), + IsMatchLocalConfigModel( + storedFeatureToggles = emptyMap(), + expected = false, + ), + ) + } } data class IsMatchLocalConfigModel( - val fileFeatureToggles: Map, val storedFeatureToggles: Map, val expected: Boolean, ) @@ -325,41 +341,40 @@ internal class DevFeatureTogglesManagerTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class GetFeatureToggles { + @BeforeEach + fun setupEach() { + every { featureTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) + } + @Test fun getFeatureToggles() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" - - val fileFeatureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", - "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + val appVersion = "1.0.0" + val fileToggles = getExpectedFileToggles(appVersion) + val savedToggles = mapOf( + "DISABLED_TOGGLE" to true, + "ENABLED_TOGGLE" to false, ) - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns fileFeatureToggles - - val savedFeatureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to true, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles - - val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) // Act val actual = manager.getFeatureToggles() // Assert - val expected = savedFeatureToggles + val expected = fileToggles + savedToggles Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { versionProvider.get() featureTogglesLocalStorage.getSyncOrEmpty() } - - clearMocks(versionProvider, featureTogglesLocalStorage) - unmockkObject(FeatureToggles) } } @@ -367,25 +382,30 @@ internal class DevFeatureTogglesManagerTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class ChangeToggle { + @BeforeEach + fun setupEach() { + every { featureTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) + } + @ParameterizedTest @ProvideTestModels fun changeToggle(model: ChangeToggleModel) = runTest { // Arrange every { versionProvider.get() } returns "1.0.0" - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns model.initialToggles coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap() - - val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) // Act manager.changeToggle(name = model.name, isEnabled = model.isEnabled) val actual = manager.getFeatureToggles() // Assert - val expected = model.expectedToggles - Truth.assertThat(actual).containsExactlyEntriesIn(expected) + Truth.assertThat(actual).containsExactlyEntriesIn(model.expectedToggles) coVerifyOrder { versionProvider.get() @@ -395,77 +415,69 @@ internal class DevFeatureTogglesManagerTest { featureTogglesLocalStorage.store(model.expectedToggles) } } - - clearMocks(versionProvider, featureTogglesLocalStorage) - unmockkObject(FeatureToggles) } - private fun provideTestModels() = listOf( - ChangeToggleModel( - initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"), - name = "TOGGLE_1", - isEnabled = false, - expectedToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false), - expectedStoreSaving = true, - ), - ChangeToggleModel( - initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"), - name = "TOGGLE_2", - isEnabled = true, - expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true), - expectedStoreSaving = true, - ), - ChangeToggleModel( - initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"), - name = "TOGGLE_3", - isEnabled = true, - expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false), - expectedStoreSaving = false, - ), - ChangeToggleModel( - initialToggles = emptyMap(), - name = "TOGGLE_1", - isEnabled = true, - expectedToggles = emptyMap(), - expectedStoreSaving = false, - ), - ) + private fun provideTestModels(): List { + val appVersion = "1.0.0" + val fileToggles = getExpectedFileToggles(appVersion) + val enabledName = "ENABLED_TOGGLE" + val disabledName = "DISABLED_TOGGLE" + + return listOf( + ChangeToggleModel( + name = enabledName, + isEnabled = false, + expectedToggles = fileToggles + (enabledName to false), + expectedStoreSaving = true, + ), + ChangeToggleModel( + name = disabledName, + isEnabled = true, + expectedToggles = fileToggles + (disabledName to true), + expectedStoreSaving = true, + ), + ChangeToggleModel( + name = "UNKNOWN_TOGGLE", + isEnabled = true, + expectedToggles = fileToggles, + expectedStoreSaving = false, + ), + ) + } } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class RecoverLocalConfig { + @BeforeEach + fun setupEach() { + every { featureTogglesProvider.getToggles() } returns testToggles + } + + @AfterEach + fun tearDownEach() { + clearMocks(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) + } + @Test fun recoverLocalConfig() = runTest { // Arrange - every { versionProvider.get() } returns "1.0.0" - - val fileFeatureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", - "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", + val appVersion = "1.0.0" + val savedToggles = mapOf( + "DISABLED_TOGGLE" to true, + "ENABLED_TOGGLE" to false, ) - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns fileFeatureToggles - - val savedFeatureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to true, - "ACTIVE2_TEST_FEATURE_ENABLED" to false, - ) - coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles - - val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) + every { versionProvider.get() } returns appVersion + coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedToggles + val manager = DevFeatureTogglesManager(versionProvider, featureTogglesProvider, featureTogglesLocalStorage) // Act manager.recoverLocalConfig() val actual = manager.getFeatureToggles() // Assert - val expected = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to false, - "ACTIVE2_TEST_FEATURE_ENABLED" to true, - ) + val expected = getExpectedFileToggles(appVersion) Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { @@ -473,14 +485,10 @@ internal class DevFeatureTogglesManagerTest { featureTogglesLocalStorage.getSyncOrEmpty() featureTogglesLocalStorage.store(expected) } - - clearMocks(versionProvider, featureTogglesLocalStorage) - unmockkObject(FeatureToggles) } } data class ChangeToggleModel( - val initialToggles: Map, val name: String, val isEnabled: Boolean, val expectedToggles: Map, diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt index 8d02cfbb5b..0f861c4398 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdFeatureTogglesManagerTest.kt @@ -1,43 +1,47 @@ package com.tangem.core.configtoggle.manager import com.google.common.truth.Truth -import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager +import com.tangem.core.configtoggle.feature.provider.FeatureTogglesProvider import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.test.core.ProvideTestModels -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.* import org.junit.jupiter.params.ParameterizedTest -/** -[REDACTED_AUTHOR] - */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class ProdFeatureTogglesManagerTest { private val versionProvider = mockk() + private val featureTogglesProvider = mockk() + + private val testToggles = mapOf( + "ENABLED_TOGGLE" to "1.0.0", + "DISABLED_TOGGLE" to "undefined", + ) + + private fun getExpectedToggles(appVersion: String?): Map { + return testToggles.mapValues { (_, version) -> + version != "undefined" && !appVersion.isNullOrEmpty() + } + } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Initialization { - @BeforeAll - fun setupAll() { - val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0") - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns featureToggles - } - - @AfterAll - fun tearDownAll() { - unmockkObject(FeatureToggles) + @BeforeEach + fun setupEach() { + every { featureTogglesProvider.getToggles() } returns testToggles } @AfterEach fun tearDownEach() { - clearMocks(versionProvider) + clearMocks(versionProvider, featureTogglesProvider) } @Test @@ -47,10 +51,10 @@ internal class ProdFeatureTogglesManagerTest { every { versionProvider.get() } returns appVersion // Act - val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles() + val actual = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider).getProdFeatureToggles() // Assert - val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false) + val expected = getExpectedToggles(appVersion) Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { versionProvider.get() } @@ -59,14 +63,14 @@ internal class ProdFeatureTogglesManagerTest { @Test fun `successfully initialize storage if versionProvider returns null`() = runTest { // Arrange - val appVersion = null + val appVersion: String? = null every { versionProvider.get() } returns appVersion // Act - val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles() + val actual = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider).getProdFeatureToggles() // Assert - val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false) + val expected = getExpectedToggles(appVersion) Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { versionProvider.get() } @@ -79,10 +83,10 @@ internal class ProdFeatureTogglesManagerTest { every { versionProvider.get() } returns appVersion // Act - val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles() + val actual = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider).getProdFeatureToggles() // Assert - val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false) + val expected = getExpectedToggles(appVersion) Truth.assertThat(actual).containsExactlyEntriesIn(expected) coVerifyOrder { versionProvider.get() } @@ -95,7 +99,8 @@ internal class ProdFeatureTogglesManagerTest { every { versionProvider.get() } throws exception // Act - val actual = runCatching { ProdFeatureTogglesManager(versionProvider) }.exceptionOrNull()!! + val actual = runCatching { ProdFeatureTogglesManager(versionProvider, featureTogglesProvider) } + .exceptionOrNull()!! // Assert Truth.assertThat(actual).isInstanceOf(exception::class.java) @@ -113,23 +118,14 @@ internal class ProdFeatureTogglesManagerTest { @BeforeAll fun setupAll() { + every { featureTogglesProvider.getToggles() } returns testToggles every { versionProvider.get() } returns "1.0.0" - - val featureToggles = mapOf( - "INACTIVE_TEST_FEATURE_ENABLED" to "undefined", - "ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0", - ) - - mockkObject(FeatureToggles) - every { FeatureToggles.values } returns featureToggles - - manager = ProdFeatureTogglesManager(versionProvider) + manager = ProdFeatureTogglesManager(versionProvider, featureTogglesProvider) } @AfterAll fun tearDownAll() { - clearMocks(versionProvider) - unmockkObject(FeatureToggles) + clearMocks(versionProvider, featureTogglesProvider) } @ParameterizedTest @@ -139,15 +135,12 @@ internal class ProdFeatureTogglesManagerTest { val actual = manager.isFeatureEnabled(name = model.name) // Assert - val expected = model.expected - Truth.assertThat(actual).isEqualTo(expected) + Truth.assertThat(actual).isEqualTo(model.expected) } private fun provideTestModels() = listOf( - IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = true), - IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = false), - IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false), - IsFeatureEnabledModel(name = "", expected = false), + IsFeatureEnabledModel(name = "ENABLED_TOGGLE", expected = true), + IsFeatureEnabledModel(name = "DISABLED_TOGGLE", expected = false), ) } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TogglesGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TogglesGenerator.kt index be4edcd2f0..59795077a6 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TogglesGenerator.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TogglesGenerator.kt @@ -1,7 +1,6 @@ package com.tangem.plugin.configuration.configurations import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject @@ -12,8 +11,7 @@ import java.util.Locale /** * Generator for toggle configuration Kotlin enum classes from JSON files. * Parses a JSON array of `{ "name": "...", "version": "..." }` entries - * and generates an enum class with an entry for each toggle and a `values: Map` - * in companion object mapping toggle name to its version string. + * and generates an enum class with `rawName` and `version` properties for each toggle. * [REDACTED_AUTHOR] */ @@ -39,31 +37,35 @@ object TogglesGenerator { ToggleEntry(name = name, enumName = name.toEnumEntryName(), version = version) } - val mapInitializer = buildCodeBlock { - addStatement("mapOf(") - withIndent { - entries.forEach { entry -> - addStatement("%S to %S,", entry.name, entry.version) - } - } - add(")") - } - - val companionBuilder = TypeSpec.companionObjectBuilder() - .addProperty( - PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING)) - .initializer(mapInitializer) - .build(), - ) - val enumBuilder = TypeSpec.enumBuilder(fileName) .addKdoc("Generated from ${inputFile.name}\nAuto-generated - do not edit manually.") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(ParameterSpec.builder("rawName", String::class).build()) + .addParameter(ParameterSpec.builder("version", String::class).build()) + .build(), + ) + .addProperty( + PropertySpec.builder("rawName", String::class) + .initializer("rawName") + .build(), + ) + .addProperty( + PropertySpec.builder("version", String::class) + .initializer("version") + .build(), + ) .apply { entries.forEach { entry -> - addEnumConstant(entry.enumName) + addEnumConstant( + entry.enumName, + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%S", entry.name) + .addSuperclassConstructorParameter("%S", entry.version) + .build(), + ) } } - .addType(companionBuilder.build()) val fileSpec = FileSpec.builder(packageName = PACKAGE_NAME, fileName = fileName) .indent(" ") @@ -73,7 +75,6 @@ object TogglesGenerator { outputDir.mkdirs() fileSpec.writeTo(outputDir) - // Remove redundant public visibility modifiers val generatedFile = File(outputDir, "${PACKAGE_NAME.replace('.', '/')}/$fileName.kt") if (generatedFile.exists()) { val content = generatedFile.readText() @@ -81,6 +82,7 @@ object TogglesGenerator { .replace("public enum class ", "enum class ") .replace("public companion object", "companion object") .replace("public val ", "val ") + .replace(Regex(""",\s*;\s*\n\s*}"""), ",\n}") generatedFile.writeText(fixedContent) } } diff --git a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/TogglesGeneratorTest.kt b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/TogglesGeneratorTest.kt index 27d25144db..9b084026aa 100644 --- a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/TogglesGeneratorTest.kt +++ b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/TogglesGeneratorTest.kt @@ -42,11 +42,25 @@ class TogglesGeneratorTest { ) // Assert - assertThat(code).contains("enum class FeatureToggles {") + assertThat(code).contains("enum class FeatureToggles(") } @Test - fun `generate creates enum entries and companion object`() { + fun `generate creates enum with constructor parameters`() { + // Arrange + val json = """[{ "name": "onramp/ios", "version": "1.0.0" }]""" + + // Act + val code = generateAndReadOutput(json, "Toggles") + + // Assert + assertThat(code).contains("ONRAMP_IOS(") + assertThat(code).contains("val rawName: String") + assertThat(code).contains("val version: String") + } + + @Test + fun `generate creates enum entries without companion object`() { // Arrange & Act val code = generateAndReadOutput( json = SINGLE_ENTRY_JSON, @@ -54,21 +68,23 @@ class TogglesGeneratorTest { ) // Assert - assertThat(code).contains("STAKING_ETH_ENABLED,") - assertThat(code).contains("companion object {") + assertThat(code).contains("STAKING_ETH_ENABLED(") + assertThat(code).contains("val rawName: String") + assertThat(code).contains("val version: String") + assertThat(code).doesNotContain("companion object") + assertThat(code).doesNotContain("val values:") } @Test - fun `generate creates values map with string literal keys`() { + fun `generate creates enum entries with constructor arguments`() { // Arrange & Act val code = generateAndReadOutput( json = SINGLE_ENTRY_JSON, fileName = "FeatureToggles", ) - // Assert - assertThat(code).contains("val values: Map = mapOf(") - assertThat(code).contains(""""STAKING_ETH_ENABLED" to "undefined"""") + // Assert - verify enum entry includes constructor arguments + assertThat(code).contains(""""STAKING_ETH_ENABLED", "undefined"""") } @Test @@ -86,12 +102,12 @@ class TogglesGeneratorTest { val code = generateAndReadOutput(json, "TestToggles") // Assert - assertThat(code).contains("FEATURE_A,") - assertThat(code).contains("FEATURE_B,") - assertThat(code).contains("FEATURE_C,") - assertThat(code).contains(""""FEATURE_A" to "1.0"""") - assertThat(code).contains(""""FEATURE_B" to "2.0"""") - assertThat(code).contains(""""FEATURE_C" to "undefined"""") + assertThat(code).contains("FEATURE_A(") + assertThat(code).contains("FEATURE_B(") + assertThat(code).contains("FEATURE_C(") + assertThat(code).contains(""""FEATURE_A", "1.0"""") + assertThat(code).contains(""""FEATURE_B", "2.0"""") + assertThat(code).contains(""""FEATURE_C", "undefined"""") } @Test @@ -103,8 +119,8 @@ class TogglesGeneratorTest { ) // Assert - assertThat(code).contains("enum class EmptyToggles {") - assertThat(code).contains("val values: Map = mapOf(") + assertThat(code).contains("enum class EmptyToggles(") + assertThat(code).doesNotContain("companion object") } @Test @@ -116,8 +132,8 @@ class TogglesGeneratorTest { val code = generateAndReadOutput(json, "Toggles") // Assert - assertThat(code).contains("NEXA_TEST,") - assertThat(code).contains(""""NEXA/test" to "undefined"""") + assertThat(code).contains("NEXA_TEST(") + assertThat(code).contains(""""NEXA/test", "undefined"""") } @Test @@ -129,8 +145,8 @@ class TogglesGeneratorTest { val code = generateAndReadOutput(json, "Toggles") // Assert - assertThat(code).contains("VANAR_CHAIN,") - assertThat(code).contains(""""vanar-chain" to "undefined"""") + assertThat(code).contains("VANAR_CHAIN(") + assertThat(code).contains(""""vanar-chain", "undefined"""") } @Test @@ -142,8 +158,8 @@ class TogglesGeneratorTest { val code = generateAndReadOutput(json, "Toggles") // Assert - assertThat(code).contains("SONIC,") - assertThat(code).contains(""""sonic" to "5.21.0"""") + assertThat(code).contains("SONIC(") + assertThat(code).contains(""""sonic", "5.21.0"""") } @Test @@ -169,7 +185,6 @@ class TogglesGeneratorTest { // Assert assertThat(code).doesNotContain("public enum class") - assertThat(code).doesNotContain("public companion object") assertThat(code).doesNotContain("public val") } @@ -188,12 +203,12 @@ class TogglesGeneratorTest { val code = generateAndReadOutput(json, "FeatureToggles") // Assert - assertThat(code).contains("NEW_CARD_SCANNING_ENABLED,") - assertThat(code).contains("HOT_WALLET_CREATION_RESTRICTION_ENABLED,") - assertThat(code).contains("SWAP_MARKET_LIST_ENABLED,") - assertThat(code).contains(""""NEW_CARD_SCANNING_ENABLED" to "undefined"""") - assertThat(code).contains(""""HOT_WALLET_CREATION_RESTRICTION_ENABLED" to "5.32.0"""") - assertThat(code).contains(""""SWAP_MARKET_LIST_ENABLED" to "5.34"""") + assertThat(code).contains("NEW_CARD_SCANNING_ENABLED(") + assertThat(code).contains("HOT_WALLET_CREATION_RESTRICTION_ENABLED(") + assertThat(code).contains("SWAP_MARKET_LIST_ENABLED(") + assertThat(code).contains(""""NEW_CARD_SCANNING_ENABLED", "undefined"""") + assertThat(code).contains(""""HOT_WALLET_CREATION_RESTRICTION_ENABLED", "5.32.0"""") + assertThat(code).contains(""""SWAP_MARKET_LIST_ENABLED", "5.34"""") } @Test @@ -211,13 +226,15 @@ class TogglesGeneratorTest { val code = generateAndReadOutput(json, "ExcludedBlockchainToggles") // Assert - assertThat(code).contains("enum class ExcludedBlockchainToggles {") - assertThat(code).contains("NEXA,") - assertThat(code).contains("NEXA_TEST,") - assertThat(code).contains("SONIC,") - assertThat(code).contains(""""NEXA" to "undefined"""") - assertThat(code).contains(""""NEXA/test" to "undefined"""") - assertThat(code).contains(""""sonic" to "5.21.0"""") + assertThat(code).contains("enum class ExcludedBlockchainToggles(") + assertThat(code).contains("val rawName: String") + assertThat(code).contains("val version: String") + assertThat(code).contains("NEXA(") + assertThat(code).contains("NEXA_TEST(") + assertThat(code).contains("SONIC(") + assertThat(code).contains(""""NEXA", "undefined"""") + assertThat(code).contains(""""NEXA/test", "undefined"""") + assertThat(code).contains(""""sonic", "5.21.0"""") } @Test @@ -230,8 +247,25 @@ class TogglesGeneratorTest { val code2 = generateAndReadOutput(json, "ExcludedBlockchainToggles") // Assert - assertThat(code1).contains("enum class FeatureToggles {") - assertThat(code2).contains("enum class ExcludedBlockchainToggles {") + assertThat(code1).contains("enum class FeatureToggles(") + assertThat(code2).contains("enum class ExcludedBlockchainToggles(") + } + + @Test + fun `generate does not include semicolon after enum entries`() { + // Arrange + val json = """ + [ + { "name": "FEATURE_A", "version": "1.0" }, + { "name": "FEATURE_B", "version": "2.0" } + ] + """.trimIndent() + + // Act + val code = generateAndReadOutput(json, "TestToggles") + + // Assert - ensure no trailing semicolons + assertThat(code).doesNotContain(";\n") } private fun generateAndReadOutput(json: String, fileName: String): String {