Skip to content

Commit

Permalink
[BWA-59] Define feature flag manager (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintPatrck authored Aug 29, 2024
1 parent 0d77e70 commit b49d8a1
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.bitwarden.authenticator.data.platform.manager

import com.bitwarden.authenticator.data.platform.manager.model.FeatureFlag
import kotlinx.coroutines.flow.Flow

/**
* Manages the available feature flags for the Bitwarden application.
*/
interface FeatureFlagManager {

/**
* Returns a flow emitting the value of flag [key] which is of generic type [T].
* If the value of the flag cannot be retrieved, the default value of [key] will be returned
*/
fun <T : Any> getFeatureFlagFlow(key: FeatureFlag<T>): Flow<T>

/**
* Gets the value for feature flag with [key] and returns it as generic type [T].
* If no value is found the given [key] its [FeatureFlag.defaultValue] will be returned.
*/
fun <T : Any> getFeatureFlag(
key: FeatureFlag<T>,
): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bitwarden.authenticator.data.platform.manager

import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.manager.model.FeatureFlag
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* Primary implementation of [FeatureFlagManager].
*/
class FeatureFlagManagerImpl(
private val featureFlagRepository: FeatureFlagRepository,
) : FeatureFlagManager {

override fun <T : Any> getFeatureFlagFlow(key: FeatureFlag<T>): Flow<T> =
featureFlagRepository
.featureFlagConfigStateFlow
.map { serverConfig ->
serverConfig.getFlagValueOrDefault(key = key)
}

override fun <T : Any> getFeatureFlag(key: FeatureFlag<T>): T =
featureFlagRepository
.featureFlagConfigStateFlow
.value
.getFlagValueOrDefault(key = key)
}

private fun <T : Any> FeatureFlagsConfiguration?.getFlagValueOrDefault(key: FeatureFlag<T>): T {
val defaultValue = key.defaultValue
return this
?.featureFlags
?.get(key.name)
?.let {
try {
// Suppressed since we are checking the type before doing the cast
@Suppress("UNCHECKED_CAST")
when (defaultValue::class) {
Boolean::class -> it.content.toBoolean() as T
String::class -> it.content as T
Int::class -> it.content.toInt() as T
else -> defaultValue
}
} catch (ex: ClassCastException) {
defaultValue
} catch (ex: NumberFormatException) {
defaultValue
}
}
?: defaultValue
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import com.bitwarden.authenticator.data.platform.manager.CrashLogsManager
import com.bitwarden.authenticator.data.platform.manager.CrashLogsManagerImpl
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.manager.DispatcherManagerImpl
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManagerImpl
import com.bitwarden.authenticator.data.platform.manager.SdkClientManager
import com.bitwarden.authenticator.data.platform.manager.SdkClientManagerImpl
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManagerImpl
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -73,4 +76,10 @@ object PlatformManagerModule {
@Provides
@Singleton
fun provideEncodingManager(): BitwardenEncodingManager = BitwardenEncodingManagerImpl()

@Provides
@Singleton
fun provideFeatureFlagManager(
featureFlagRepository: FeatureFlagRepository,
): FeatureFlagManager = FeatureFlagManagerImpl(featureFlagRepository)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.bitwarden.authenticator.data.platform.manager

import app.cash.turbine.test
import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import com.bitwarden.authenticator.data.platform.repository.util.FakeFeatureFlagRepository
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue

class FeatureFlagManagerTest {
private val fakeFeatureFlagRepository = FakeFeatureFlagRepository()

private val featureFlagManager = FeatureFlagManagerImpl(
featureFlagRepository = fakeFeatureFlagRepository,
)

@Test
fun `FeatureFlagRepository flow with value should trigger new flags`() = runTest {
fakeFeatureFlagRepository.featureFlagsConfiguration = null
assertNull(
fakeFeatureFlagRepository.featureFlagsConfiguration,
)

fakeFeatureFlagRepository.featureFlagsConfiguration = FEATURE_FLAGS_CONFIG

featureFlagManager
.getFeatureFlagFlow(LocalFeatureFlag.BitwardenAuthenticationEnabled)
.test {
assertNotNull(awaitItem())
}
}

@Test
fun `getFeatureFlag should return value if exists`() = runTest {
fakeFeatureFlagRepository.featureFlagsConfiguration = FEATURE_FLAGS_CONFIG

val flagValue = featureFlagManager.getFeatureFlag(
key = LocalFeatureFlag.BitwardenAuthenticationEnabled,
)

assertTrue(flagValue)
}

@Test
fun `getFeatureFlag should return default value if flag doesn't exist`() {
fakeFeatureFlagRepository.featureFlagsConfiguration = FEATURE_FLAGS_CONFIG.copy(
featureFlags = emptyMap()
)

val flagValue = featureFlagManager.getFeatureFlag(
LocalFeatureFlag.BitwardenAuthenticationEnabled,
)

assertFalse(flagValue)
}
}

private val FEATURE_FLAGS_CONFIG =
FeatureFlagsConfiguration(
mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to
JsonPrimitive(true),
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.bitwarden.authenticator.data.platform.repository.util

import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.json.JsonPrimitive

/**
* Faked implementation of [FeatureFlagRepository] for testing.
*/
class FakeFeatureFlagRepository : FeatureFlagRepository {
var featureFlagsConfiguration: FeatureFlagsConfiguration?
get() = mutableFeatureFlagsConfiguration.value
set(value) {
mutableFeatureFlagsConfiguration.value = value
}

private val mutableFeatureFlagsConfiguration =
MutableStateFlow<FeatureFlagsConfiguration?>(FEATURE_FLAGS_CONFIG)

override val featureFlagConfigStateFlow: StateFlow<FeatureFlagsConfiguration?> =
mutableFeatureFlagsConfiguration

override suspend fun getFeatureFlagsConfiguration(): FeatureFlagsConfiguration {
return featureFlagsConfiguration
?: FEATURE_FLAGS_CONFIG
}
}

private val FEATURE_FLAGS_CONFIG =
FeatureFlagsConfiguration(
featureFlags = mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to
JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue)
)
)

0 comments on commit b49d8a1

Please sign in to comment.