PM-15110: Ensure all network requests always use the current environment data (#4344)

This commit is contained in:
David Perez 2024-11-20 13:36:43 -06:00 committed by GitHub
parent 5ea17700b3
commit 3092ba1fc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 66 additions and 196 deletions

View file

@ -6,37 +6,27 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
/** /**
* A [Interceptor] that optionally takes the current base URL of a request and replaces it with * An [Interceptor] that optionally takes the current base URL of a request and replaces it with
* the currently set [baseUrl] * the currently set base URL from the [baseUrlProvider].
*/ */
class BaseUrlInterceptor : Interceptor { class BaseUrlInterceptor(
private val baseUrlProvider: () -> String?,
) : Interceptor {
/** private val baseHttpUrl: HttpUrl?
* The base URL to use as an override, or `null` if no override should be performed. get() = baseUrlProvider()?.let { requireNotNull(it.toHttpUrlOrNull()) }
*/
var baseUrl: String? = null
set(value) {
field = value
baseHttpUrl = baseUrl?.let { requireNotNull(it.toHttpUrlOrNull()) }
}
private var baseHttpUrl: HttpUrl? = null
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
// If no base URL is set, we can simply skip // If no base URL is set, we can simply skip
val base = baseHttpUrl ?: return chain.proceed(request) val base = baseHttpUrl ?: return chain.proceed(request = request)
// Update the base URL used. // Update the base URL used.
return chain.proceed( return chain.proceed(
request request = request
.newBuilder() .newBuilder()
.url( .url(url = request.url.replaceBaseUrlWith(baseUrl = base))
request
.url
.replaceBaseUrlWith(base),
)
.build(), .build(),
) )
} }

View file

@ -1,47 +1,44 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseApiUrl import com.x8bit.bitwarden.data.platform.repository.util.baseApiUrl
import com.x8bit.bitwarden.data.platform.repository.util.baseEventsUrl import com.x8bit.bitwarden.data.platform.repository.util.baseEventsUrl
import com.x8bit.bitwarden.data.platform.repository.util.baseIdentityUrl import com.x8bit.bitwarden.data.platform.repository.util.baseIdentityUrl
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
/** /**
* An overall container for various [BaseUrlInterceptor] implementations for different API groups. * An overall container for various [BaseUrlInterceptor] implementations for different API groups.
*/ */
@OmitFromCoverage
@Singleton @Singleton
class BaseUrlInterceptors @Inject constructor() { class BaseUrlInterceptors @Inject constructor(
var environment: Environment = Environment.Us private val environmentDiskSource: EnvironmentDiskSource,
set(value) { ) {
field = value private val environment: Environment
updateBaseUrls(environment = value) get() = environmentDiskSource.preAuthEnvironmentUrlData.toEnvironmentUrlsOrDefault()
}
/** /**
* An interceptor for "/api" calls. * An interceptor for "/api" calls.
*/ */
val apiInterceptor: BaseUrlInterceptor = BaseUrlInterceptor() val apiInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
environment.environmentUrlData.baseApiUrl
}
/** /**
* An interceptor for "/identity" calls. * An interceptor for "/identity" calls.
*/ */
val identityInterceptor: BaseUrlInterceptor = BaseUrlInterceptor() val identityInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
environment.environmentUrlData.baseIdentityUrl
}
/** /**
* An interceptor for "/events" calls. * An interceptor for "/events" calls.
*/ */
val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor() val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
environment.environmentUrlData.baseEventsUrl
init {
// Ensure all interceptors begin with a default value
environment = Environment.Us
}
private fun updateBaseUrls(environment: Environment) {
val environmentUrlData = environment.environmentUrlData
apiInterceptor.baseUrl = environmentUrlData.baseApiUrl
identityInterceptor.baseUrl = environmentUrlData.baseIdentityUrl
eventsInterceptor.baseUrl = environmentUrlData.baseEventsUrl
} }
} }

View file

@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
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.ServerConfigRepository
@ -20,7 +19,6 @@ class NetworkConfigManagerImpl(
authRepository: AuthRepository, authRepository: AuthRepository,
environmentRepository: EnvironmentRepository, environmentRepository: EnvironmentRepository,
serverConfigRepository: ServerConfigRepository, serverConfigRepository: ServerConfigRepository,
private val baseUrlInterceptors: BaseUrlInterceptors,
refreshAuthenticator: RefreshAuthenticator, refreshAuthenticator: RefreshAuthenticator,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
) : NetworkConfigManager { ) : NetworkConfigManager {
@ -31,9 +29,6 @@ class NetworkConfigManagerImpl(
@Suppress("OPT_IN_USAGE") @Suppress("OPT_IN_USAGE")
environmentRepository environmentRepository
.environmentStateFlow .environmentStateFlow
.onEach { environment ->
baseUrlInterceptors.environment = environment
}
.debounce(timeoutMillis = ENVIRONMENT_DEBOUNCE_TIMEOUT_MS) .debounce(timeoutMillis = ENVIRONMENT_DEBOUNCE_TIMEOUT_MS)
.onEach { _ -> .onEach { _ ->
// This updates the stored service configuration by performing a network request. // This updates the stored service configuration by performing a network request.

View file

@ -12,7 +12,6 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.AppStateManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
@ -198,7 +197,6 @@ object PlatformManagerModule {
authRepository: AuthRepository, authRepository: AuthRepository,
environmentRepository: EnvironmentRepository, environmentRepository: EnvironmentRepository,
serverConfigRepository: ServerConfigRepository, serverConfigRepository: ServerConfigRepository,
baseUrlInterceptors: BaseUrlInterceptors,
refreshAuthenticator: RefreshAuthenticator, refreshAuthenticator: RefreshAuthenticator,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
): NetworkConfigManager = ): NetworkConfigManager =
@ -206,7 +204,6 @@ object PlatformManagerModule {
authRepository = authRepository, authRepository = authRepository,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,
serverConfigRepository = serverConfigRepository, serverConfigRepository = serverConfigRepository,
baseUrlInterceptors = baseUrlInterceptors,
refreshAuthenticator = refreshAuthenticator, refreshAuthenticator = refreshAuthenticator,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
) )

View file

@ -0,0 +1,34 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
class FakeEnvironmentDiskSource : EnvironmentDiskSource {
private val storedEmailVerificationUrls = mutableMapOf<String, EnvironmentUrlDataJson?>()
override var preAuthEnvironmentUrlData: EnvironmentUrlDataJson? = null
set(value) {
field = value
mutablePreAuthEnvironmentUrlDataFlow.tryEmit(value)
}
override val preAuthEnvironmentUrlDataFlow: Flow<EnvironmentUrlDataJson?>
get() = mutablePreAuthEnvironmentUrlDataFlow
.onSubscription { emit(preAuthEnvironmentUrlData) }
override fun getPreAuthEnvironmentUrlDataForEmail(
userEmail: String,
): EnvironmentUrlDataJson? = storedEmailVerificationUrls[userEmail]
override fun storePreAuthEnvironmentUrlDataForEmail(
userEmail: String,
urls: EnvironmentUrlDataJson,
) {
storedEmailVerificationUrls[userEmail] = urls
}
private val mutablePreAuthEnvironmentUrlDataFlow =
bufferedMutableSharedFlow<EnvironmentUrlDataJson?>(replay = 1)
}

View file

@ -6,7 +6,8 @@ import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class BaseUrlInterceptorTest { class BaseUrlInterceptorTest {
private val baseUrlInterceptor = BaseUrlInterceptor() private var baseUrl: String? = null
private val baseUrlInterceptor = BaseUrlInterceptor { baseUrl }
@Test @Test
fun `intercept with a null base URL should proceed with the original request`() { fun `intercept with a null base URL should proceed with the original request`() {
@ -22,7 +23,7 @@ class BaseUrlInterceptorTest {
@Test @Test
fun `intercept with a non-null base URL should update the base URL used by the request`() { fun `intercept with a non-null base URL should update the base URL used by the request`() {
baseUrlInterceptor.baseUrl = "https://api.bitwarden.com" baseUrl = "https://api.bitwarden.com"
val request = Request.Builder().url("http://www.fake.com/").build() val request = Request.Builder().url("http://www.fake.com/").build()
val chain = FakeInterceptorChain(request) val chain = FakeInterceptorChain(request)

View file

@ -1,101 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class BaseUrlInterceptorsTest {
private val baseUrlInterceptors = BaseUrlInterceptors()
@Test
fun `the default environment should be US and all interceptors should have the correct URLs`() {
assertEquals(
Environment.Us,
baseUrlInterceptors.environment,
)
assertEquals(
"https://vault.bitwarden.com/api",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.com/identity",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.com/events",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
@Suppress("MaxLineLength")
@Test
fun `setting the environment should update all the interceptors correctly for a non-blank base URL`() {
baseUrlInterceptors.environment = Environment.Eu
assertEquals(
"https://vault.bitwarden.eu/api",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.eu/identity",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.eu/events",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
@Suppress("MaxLineLength")
@Test
fun `setting the environment should update all the interceptors correctly for a blank base URL and all URLs filled`() {
baseUrlInterceptors.environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = " ",
api = "https://api.com",
identity = "https://identity.com",
events = "https://events.com",
),
)
assertEquals(
"https://api.com",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://identity.com",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://events.com",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
@Suppress("MaxLineLength")
@Test
fun `setting the environment should update all the interceptors correctly for a blank base URL and some or all URLs absent`() {
baseUrlInterceptors.environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = " ",
api = "",
identity = "",
icon = " ",
),
)
assertEquals(
"https://api.bitwarden.com",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://identity.bitwarden.com",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://events.bitwarden.com",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
}

View file

@ -4,7 +4,6 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
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.ServerConfigRepository
@ -17,7 +16,6 @@ import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -42,7 +40,6 @@ class NetworkConfigManagerTest {
} }
private val refreshAuthenticator = RefreshAuthenticator() private val refreshAuthenticator = RefreshAuthenticator()
private val baseUrlInterceptors = BaseUrlInterceptors()
private lateinit var networkConfigManager: NetworkConfigManager private lateinit var networkConfigManager: NetworkConfigManager
@ -52,25 +49,15 @@ class NetworkConfigManagerTest {
authRepository = authRepository, authRepository = authRepository,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,
serverConfigRepository = serverConfigRepository, serverConfigRepository = serverConfigRepository,
baseUrlInterceptors = baseUrlInterceptors,
refreshAuthenticator = refreshAuthenticator, refreshAuthenticator = refreshAuthenticator,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
) )
} }
@Test @Test
fun `changes in the Environment should update the BaseUrlInterceptors`() { fun `changes in the Environment should call getServerConfig after debounce period`() {
mutableEnvironmentStateFlow.value = Environment.Us mutableEnvironmentStateFlow.value = Environment.Us
assertEquals(
Environment.Us,
baseUrlInterceptors.environment,
)
mutableEnvironmentStateFlow.value = Environment.Eu mutableEnvironmentStateFlow.value = Environment.Eu
assertEquals(
Environment.Eu,
baseUrlInterceptors.environment,
)
testDispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 500L) testDispatcher.advanceTimeByAndRunCurrent(delayTimeMillis = 500L)
coVerify(exactly = 1) { coVerify(exactly = 1) {
serverConfigRepository.getServerConfig(forceRefresh = true) serverConfigRepository.getServerConfig(forceRefresh = true)

View file

@ -6,17 +6,14 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJso
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.FakeEnvironmentDiskSource
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.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@ -218,30 +215,3 @@ class EnvironmentRepositoryTest {
} }
private const val EMAIL = "email@example.com" private const val EMAIL = "email@example.com"
private class FakeEnvironmentDiskSource : EnvironmentDiskSource {
private val storedEmailVerificationUrls = mutableMapOf<String, EnvironmentUrlDataJson?>()
override var preAuthEnvironmentUrlData: EnvironmentUrlDataJson? = null
set(value) {
field = value
mutablePreAuthEnvironmentUrlDataFlow.tryEmit(value)
}
override val preAuthEnvironmentUrlDataFlow: Flow<EnvironmentUrlDataJson?>
get() = mutablePreAuthEnvironmentUrlDataFlow
.onSubscription { emit(preAuthEnvironmentUrlData) }
override fun getPreAuthEnvironmentUrlDataForEmail(userEmail: String): EnvironmentUrlDataJson? =
storedEmailVerificationUrls[userEmail]
override fun storePreAuthEnvironmentUrlDataForEmail(
userEmail: String,
urls: EnvironmentUrlDataJson,
) {
storedEmailVerificationUrls[userEmail] = urls
}
private val mutablePreAuthEnvironmentUrlDataFlow =
bufferedMutableSharedFlow<EnvironmentUrlDataJson?>(replay = 1)
}