mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 01:16:02 +03:00
PM-10835: Make config request after environment update (#3720)
This commit is contained in:
parent
4bd81782c8
commit
551f948644
8 changed files with 47 additions and 57 deletions
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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?>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue