[BWA-58] Define feature flag repo (#176)

This commit is contained in:
Patrick Honkonen
2024-08-29 10:26:24 -04:00
committed by GitHub
parent d0203eedc4
commit 0d77e7085b
6 changed files with 198 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import kotlinx.coroutines.flow.StateFlow
/**
* Provides an API for observing the server config state.
*/
interface FeatureFlagRepository {
/**
* Emits updates that track [FeatureFlagsConfiguration].
*/
val featureFlagConfigStateFlow: StateFlow<FeatureFlagsConfiguration?>
/**
* Gets the state [FeatureFlagsConfiguration].
*/
suspend fun getFeatureFlagsConfiguration(): FeatureFlagsConfiguration
}

View File

@@ -0,0 +1,47 @@
package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.serialization.json.JsonPrimitive
/**
* Primary implementation of [FeatureFlagRepositoryImpl].
*/
class FeatureFlagRepositoryImpl(
private val featureFlagDiskSource: FeatureFlagDiskSource,
dispatcherManager: DispatcherManager,
) : FeatureFlagRepository {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
override val featureFlagConfigStateFlow: StateFlow<FeatureFlagsConfiguration?>
get() = featureFlagDiskSource
.featureFlagsConfigurationFlow
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = featureFlagDiskSource.featureFlagsConfiguration,
)
override suspend fun getFeatureFlagsConfiguration() =
featureFlagDiskSource.featureFlagsConfiguration
?: initLocalFeatureFlagsConfiguration()
private fun initLocalFeatureFlagsConfiguration(): FeatureFlagsConfiguration {
val config = FeatureFlagsConfiguration(
mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(
LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue
)
)
)
featureFlagDiskSource.featureFlagsConfiguration = config
return config
}
}

View File

@@ -2,9 +2,12 @@ package com.bitwarden.authenticator.data.platform.repository.di
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.sdk.AuthenticatorSdkSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepositoryImpl
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepositoryImpl
import dagger.Module
@@ -36,4 +39,15 @@ object PlatformRepositoryModule {
biometricsEncryptionManager = biometricsEncryptionManager,
authenticatorSdkSource = authenticatorSdkSource,
)
@Provides
@Singleton
fun provideFeatureFlagRepo(
featureFlagDiskSource: FeatureFlagDiskSource,
dispatcherManager: DispatcherManager,
): FeatureFlagRepository =
FeatureFlagRepositoryImpl(
featureFlagDiskSource = featureFlagDiskSource,
dispatcherManager = dispatcherManager,
)
}

View File

@@ -0,0 +1,37 @@
package com.bitwarden.authenticator.data.platform.base
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
/**
* A faked implementation of [DispatcherManager] that uses [UnconfinedTestDispatcher].
*/
@OptIn(ExperimentalCoroutinesApi::class)
class FakeDispatcherManager(
override val default: CoroutineDispatcher = UnconfinedTestDispatcher(),
override val io: CoroutineDispatcher = UnconfinedTestDispatcher(),
override val unconfined: CoroutineDispatcher = UnconfinedTestDispatcher(),
) : DispatcherManager {
override val main: MainCoroutineDispatcher = Dispatchers.Main
/**
* Updates the main dispatcher to use the provided [dispatcher]. Used in conjunction with
* [resetMain].
*/
fun setMain(dispatcher: CoroutineDispatcher) {
Dispatchers.setMain(dispatcher)
}
/**
* Restores the main dispatcher to it's default state. Used in conjunction with [setMain].
*/
fun resetMain() {
Dispatchers.resetMain()
}
}

View File

@@ -0,0 +1,27 @@
package com.bitwarden.authenticator.data.platform.datasource.disk.util
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
/**
* A faked implementation of [FeatureFlagDiskSource] for testing.
*/
class FakeFeatureFlagDiskSource : FeatureFlagDiskSource {
private var configuration: FeatureFlagsConfiguration? = null
private val mutableConfigurationFlow =
bufferedMutableSharedFlow<FeatureFlagsConfiguration?>(replay = 1)
override var featureFlagsConfiguration: FeatureFlagsConfiguration?
get() = configuration
set(value) {
configuration = value
mutableConfigurationFlow.tryEmit(value)
}
override val featureFlagsConfigurationFlow: Flow<FeatureFlagsConfiguration?>
get() = mutableConfigurationFlow
.onSubscription { emit(configuration) }
}

View File

@@ -0,0 +1,53 @@
package com.bitwarden.authenticator.data.platform.repository
import app.cash.turbine.test
import com.bitwarden.authenticator.data.platform.base.FakeDispatcherManager
import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.datasource.disk.util.FakeFeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
class FeatureFlagRepositoryTest {
private val fakeFeatureFlagDiskSource = FakeFeatureFlagDiskSource()
private val featureFlagRepo = FeatureFlagRepositoryImpl(
featureFlagDiskSource = fakeFeatureFlagDiskSource,
dispatcherManager = FakeDispatcherManager(),
)
@Suppress("MaxLineLength")
@Test
fun `getFeatureFlagsConfiguration should init configuration with local flags when there is none in state`() =
runTest {
assertNull(fakeFeatureFlagDiskSource.featureFlagsConfiguration)
featureFlagRepo.getFeatureFlagsConfiguration()
assertEquals(
FEATURE_FLAGS_CONFIG,
fakeFeatureFlagDiskSource.featureFlagsConfiguration
)
}
@Test
fun `featureFlagsConfigurationFlow should react to feature flag configuration changes`() =
runTest {
featureFlagRepo.getFeatureFlagsConfiguration()
featureFlagRepo.featureFlagConfigStateFlow.test {
assertEquals(fakeFeatureFlagDiskSource.featureFlagsConfiguration, awaitItem())
}
}
}
private val FEATURE_FLAGS_CONFIG =
FeatureFlagsConfiguration(
featureFlags = mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to
JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue),
)
)