PM-10835: Make config request after environment update (#3720)

This commit is contained in:
David Perez 2024-08-13 11:34:33 -05:00 committed by GitHub
parent 4bd81782c8
commit 551f948644
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 47 additions and 57 deletions

View file

@ -7,7 +7,6 @@ import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject import javax.inject.Inject
@ -33,7 +32,4 @@ class BitwardenApplication : Application() {
@Inject @Inject
lateinit var restrictionManager: RestrictionManager lateinit var restrictionManager: RestrictionManager
@Inject
lateinit var serverConfigRepository: ServerConfigRepository
} }

View file

@ -7,17 +7,23 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
private const val ENVIRONMENT_DEBOUNCE_TIMEOUT_MS: Long = 500L
/** /**
* Primary implementation of [NetworkConfigManager]. * Primary implementation of [NetworkConfigManager].
*/ */
@Suppress("LongParameterList")
class NetworkConfigManagerImpl( class NetworkConfigManagerImpl(
authRepository: AuthRepository, authRepository: AuthRepository,
private val authTokenInterceptor: AuthTokenInterceptor, private val authTokenInterceptor: AuthTokenInterceptor,
environmentRepository: EnvironmentRepository, environmentRepository: EnvironmentRepository,
serverConfigRepository: ServerConfigRepository,
private val baseUrlInterceptors: BaseUrlInterceptors, private val baseUrlInterceptors: BaseUrlInterceptors,
refreshAuthenticator: RefreshAuthenticator, refreshAuthenticator: RefreshAuthenticator,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
@ -37,11 +43,18 @@ class NetworkConfigManagerImpl(
} }
.launchIn(collectionScope) .launchIn(collectionScope)
@Suppress("OPT_IN_USAGE")
environmentRepository environmentRepository
.environmentStateFlow .environmentStateFlow
.onEach { environment -> .onEach { environment ->
baseUrlInterceptors.environment = environment baseUrlInterceptors.environment = environment
} }
.debounce(timeoutMillis = ENVIRONMENT_DEBOUNCE_TIMEOUT_MS)
.onEach { _ ->
// This updates the stored service configuration by performing a network request.
// We debounce it to avoid rapid repeated requests.
serverConfigRepository.getServerConfig(forceRefresh = true)
}
.launchIn(collectionScope) .launchIn(collectionScope)
refreshAuthenticator.authenticatorProvider = authRepository refreshAuthenticator.authenticatorProvider = authRepository

View file

@ -161,6 +161,7 @@ object PlatformManagerModule {
authRepository: AuthRepository, authRepository: AuthRepository,
authTokenInterceptor: AuthTokenInterceptor, authTokenInterceptor: AuthTokenInterceptor,
environmentRepository: EnvironmentRepository, environmentRepository: EnvironmentRepository,
serverConfigRepository: ServerConfigRepository,
baseUrlInterceptors: BaseUrlInterceptors, baseUrlInterceptors: BaseUrlInterceptors,
refreshAuthenticator: RefreshAuthenticator, refreshAuthenticator: RefreshAuthenticator,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
@ -169,6 +170,7 @@ object PlatformManagerModule {
authRepository = authRepository, authRepository = authRepository,
authTokenInterceptor = authTokenInterceptor, authTokenInterceptor = authTokenInterceptor,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,
serverConfigRepository = serverConfigRepository,
baseUrlInterceptors = baseUrlInterceptors, baseUrlInterceptors = baseUrlInterceptors,
refreshAuthenticator = refreshAuthenticator, refreshAuthenticator = refreshAuthenticator,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,

View file

@ -8,14 +8,14 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
interface ServerConfigRepository { interface ServerConfigRepository {
/**
* Emits updates that track [ServerConfig].
*/
val serverConfigStateFlow: StateFlow<ServerConfig?>
/** /**
* Gets the state [ServerConfig]. If needed or forced by [forceRefresh], * Gets the state [ServerConfig]. If needed or forced by [forceRefresh],
* updates the values using server side data. * updates the values using server side data.
*/ */
suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig?
/**
* Emits updates that track [ServerConfig].
*/
val serverConfigStateFlow: StateFlow<ServerConfig?>
} }

View file

@ -7,8 +7,6 @@ import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
@ -20,20 +18,19 @@ class ServerConfigRepositoryImpl(
private val configDiskSource: ConfigDiskSource, private val configDiskSource: ConfigDiskSource,
private val configService: ConfigService, private val configService: ConfigService,
private val clock: Clock, private val clock: Clock,
environmentRepository: EnvironmentRepository,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
) : ServerConfigRepository { ) : ServerConfigRepository {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
init { override val serverConfigStateFlow: StateFlow<ServerConfig?>
environmentRepository get() = configDiskSource
.environmentStateFlow .serverConfigFlow
.onEach { .stateIn(
getServerConfig(true) scope = unconfinedScope,
} started = SharingStarted.Eagerly,
.launchIn(unconfinedScope) initialValue = configDiskSource.serverConfig,
} )
override suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? { override suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? {
val localConfig = configDiskSource.serverConfig val localConfig = configDiskSource.serverConfig
@ -62,15 +59,6 @@ class ServerConfigRepositoryImpl(
return localConfig return localConfig
} }
override val serverConfigStateFlow: StateFlow<ServerConfig?>
get() = configDiskSource
.serverConfigFlow
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = configDiskSource.serverConfig,
)
companion object { companion object {
private const val MINIMUM_CONFIG_SYNC_INTERVAL_SEC: Long = 60 * 60 private const val MINIMUM_CONFIG_SYNC_INTERVAL_SEC: Long = 60 * 60
} }

View file

@ -37,14 +37,12 @@ object PlatformRepositoryModule {
configDiskSource: ConfigDiskSource, configDiskSource: ConfigDiskSource,
configService: ConfigService, configService: ConfigService,
clock: Clock, clock: Clock,
environmentRepository: EnvironmentRepository,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
): ServerConfigRepository = ): ServerConfigRepository =
ServerConfigRepositoryImpl( ServerConfigRepositoryImpl(
configDiskSource = configDiskSource, configDiskSource = configDiskSource,
configService = configService, configService = configService,
clock = clock, clock = clock,
environmentRepository = environmentRepository,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
) )

View file

@ -8,28 +8,40 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class NetworkConfigManagerTest { class NetworkConfigManagerTest {
private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val testDispatcher = UnconfinedTestDispatcher()
private val dispatcherManager: DispatcherManager = FakeDispatcherManager(
unconfined = testDispatcher,
)
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized) private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized)
private val mutableEnvironmentStateFlow = MutableStateFlow<Environment>(Environment.Us) private val mutableEnvironmentStateFlow = MutableStateFlow<Environment>(Environment.Us)
private val authRepository: AuthRepository = mockk { private val authRepository: AuthRepository = mockk {
every { authStateFlow } returns mutableAuthStateFlow every { authStateFlow } returns mutableAuthStateFlow
} }
private val environmentRepository: EnvironmentRepository = mockk { private val environmentRepository: EnvironmentRepository = mockk {
every { environmentStateFlow } returns mutableEnvironmentStateFlow every { environmentStateFlow } returns mutableEnvironmentStateFlow
} }
private val serverConfigRepository: ServerConfigRepository = mockk {
coEvery { getServerConfig(forceRefresh = true) } returns null
}
private val refreshAuthenticator = RefreshAuthenticator() private val refreshAuthenticator = RefreshAuthenticator()
private val authTokenInterceptor = AuthTokenInterceptor() private val authTokenInterceptor = AuthTokenInterceptor()
@ -43,6 +55,7 @@ class NetworkConfigManagerTest {
authRepository = authRepository, authRepository = authRepository,
authTokenInterceptor = authTokenInterceptor, authTokenInterceptor = authTokenInterceptor,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,
serverConfigRepository = serverConfigRepository,
baseUrlInterceptors = baseUrlInterceptors, baseUrlInterceptors = baseUrlInterceptors,
refreshAuthenticator = refreshAuthenticator, refreshAuthenticator = refreshAuthenticator,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
@ -85,5 +98,9 @@ class NetworkConfigManagerTest {
Environment.Eu, Environment.Eu,
baseUrlInterceptors.environment, baseUrlInterceptors.environment,
) )
testDispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 500L)
coVerify(exactly = 1) {
serverConfigRepository.getServerConfig(forceRefresh = true)
}
} }
} }

View file

@ -9,8 +9,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponse
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson.ServerJson import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson.ServerJson
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.asSuccess
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
@ -34,10 +32,6 @@ class ServerConfigRepositoryTest {
} returns CONFIG_RESPONSE_JSON.asSuccess() } returns CONFIG_RESPONSE_JSON.asSuccess()
} }
private val environmentRepository = FakeEnvironmentRepository().apply {
environment = Environment.Us
}
private val fixedClock: Clock = Clock.fixed( private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"), Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC, ZoneOffset.UTC,
@ -47,7 +41,6 @@ class ServerConfigRepositoryTest {
configDiskSource = fakeConfigDiskSource, configDiskSource = fakeConfigDiskSource,
configService = configService, configService = configService,
clock = fixedClock, clock = fixedClock,
environmentRepository = environmentRepository,
dispatcherManager = fakeDispatcherManager, dispatcherManager = fakeDispatcherManager,
) )
@ -56,23 +49,6 @@ class ServerConfigRepositoryTest {
fakeConfigDiskSource.serverConfig = null fakeConfigDiskSource.serverConfig = null
} }
@Test
fun `environmentRepository stateflow should trigger new server configuration`() = runTest {
assertNull(
fakeConfigDiskSource.serverConfig,
)
// This should trigger a new server config to be fetched
environmentRepository.environment = Environment.Eu
repository.serverConfigStateFlow.test {
assertEquals(
SERVER_CONFIG,
awaitItem(),
)
}
}
@Test @Test
fun `getServerConfig should fetch a new server configuration with force refresh as true`() = fun `getServerConfig should fetch a new server configuration with force refresh as true`() =
runTest { runTest {