mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-725, BIT-328: Add base URL interceptors and dynamically change environments (#160)
This commit is contained in:
parent
8bdda9bffd
commit
dc6d37dc32
18 changed files with 676 additions and 78 deletions
|
@ -5,18 +5,16 @@ import retrofit2.http.Field
|
|||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /identity API.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
|
||||
@POST
|
||||
@POST("/connect/token")
|
||||
@Suppress("LongParameterList")
|
||||
@FormUrlEncoded
|
||||
suspend fun getToken(
|
||||
@Url url: String,
|
||||
@Field(value = "scope", encoded = true) scope: String,
|
||||
@Field(value = "client_id") clientId: String,
|
||||
@Field(value = "username") email: String,
|
||||
|
|
|
@ -6,15 +6,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedSe
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedServiceImpl
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.NetworkModule
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
|
@ -27,23 +25,30 @@ object NetworkModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): AccountsService = AccountsServiceImpl(retrofit.create(), json)
|
||||
): AccountsService = AccountsServiceImpl(
|
||||
accountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(retrofit.create(), json)
|
||||
): IdentityService = IdentityServiceImpl(
|
||||
api = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesHaveIBeenPwnedService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
retrofits: Retrofits,
|
||||
): HaveIBeenPwnedService = HaveIBeenPwnedServiceImpl(
|
||||
retrofit.newBuilder()
|
||||
retrofits
|
||||
.staticRetrofitBuilder
|
||||
.baseUrl("https://api.pwnedpasswords.com")
|
||||
.build()
|
||||
.create(),
|
||||
|
|
|
@ -13,8 +13,6 @@ import java.util.UUID
|
|||
class IdentityServiceImpl constructor(
|
||||
private val api: IdentityApi,
|
||||
private val json: Json,
|
||||
// TODO: use correct base URL here BIT-328
|
||||
private val baseUrl: String = "https://vault.bitwarden.com",
|
||||
private val deviceModelProvider: DeviceModelProvider = DeviceModelProvider(),
|
||||
) : IdentityService {
|
||||
|
||||
|
@ -24,8 +22,6 @@ class IdentityServiceImpl constructor(
|
|||
captchaToken: String?,
|
||||
): Result<GetTokenResponseJson> = api
|
||||
.getToken(
|
||||
// TODO: use correct base URL here BIT-328
|
||||
url = "$baseUrl/identity/connect/token",
|
||||
scope = "api+offline_access",
|
||||
clientId = "mobile",
|
||||
authEmail = email.base64UrlEncode(),
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.di
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.LocalDateTimeSerializer
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigServiceImpl
|
||||
|
@ -13,13 +14,8 @@ import dagger.hilt.components.SingletonComponent
|
|||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
|
@ -30,13 +26,11 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
const val AUTHORIZED: String = "authorized"
|
||||
const val UNAUTHORIZED: String = "unauthorized"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigService(@Named(UNAUTHORIZED) retrofit: Retrofit): ConfigService =
|
||||
ConfigServiceImpl(retrofit.create())
|
||||
fun providesConfigService(
|
||||
retrofits: Retrofits,
|
||||
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
@ -44,50 +38,16 @@ object NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesOkHttpClientBuilder(): OkHttpClient.Builder =
|
||||
OkHttpClient.Builder().addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
},
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesRetrofitBuilder(
|
||||
json: Json,
|
||||
): Retrofit.Builder {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder().baseUrl("https://api.bitwarden.com")
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(UNAUTHORIZED)
|
||||
fun providesUnauthorizedRetrofit(
|
||||
okHttpClientBuilder: OkHttpClient.Builder,
|
||||
retrofitBuilder: Retrofit.Builder,
|
||||
): Retrofit =
|
||||
retrofitBuilder
|
||||
.client(
|
||||
okHttpClientBuilder.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(AUTHORIZED)
|
||||
fun providesAuthorizedRetrofit(
|
||||
okHttpClientBuilder: OkHttpClient.Builder,
|
||||
retrofitBuilder: Retrofit.Builder,
|
||||
fun provideRetrofits(
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
): Retrofit =
|
||||
retrofitBuilder
|
||||
.client(
|
||||
okHttpClientBuilder.addInterceptor(authTokenInterceptor).build(),
|
||||
)
|
||||
.build()
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
json: Json,
|
||||
): Retrofits =
|
||||
RetrofitsImpl(
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Provides
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* A [Interceptor] that optionally takes the current base URL of a request and replaces it with
|
||||
* the currently set [baseUrl]
|
||||
*/
|
||||
class BaseUrlInterceptor : Interceptor {
|
||||
|
||||
/**
|
||||
* The base URL to use as an override, or `null` if no override should be performed.
|
||||
*/
|
||||
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 {
|
||||
val request = chain.request()
|
||||
|
||||
// If no base URL is set, we can simply skip
|
||||
val base = baseHttpUrl ?: return chain.proceed(request)
|
||||
|
||||
// Update the base URL used.
|
||||
return chain.proceed(
|
||||
request
|
||||
.newBuilder()
|
||||
.url(
|
||||
request
|
||||
.url
|
||||
.replaceBaseUrlWith(base),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a [HttpUrl], replaces the existing base URL with the given [baseUrl].
|
||||
*/
|
||||
private fun HttpUrl.replaceBaseUrlWith(
|
||||
baseUrl: HttpUrl,
|
||||
) = baseUrl
|
||||
.newBuilder()
|
||||
.addEncodedPathSegments(
|
||||
this
|
||||
.encodedPathSegments
|
||||
.joinToString(separator = "/"),
|
||||
)
|
||||
.encodedQuery(this.encodedQuery)
|
||||
.build()
|
|
@ -0,0 +1,65 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.util.orNullIfBlank
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* An overall container for various [BaseUrlInterceptor] implementations for different API groups.
|
||||
*/
|
||||
@Singleton
|
||||
class BaseUrlInterceptors @Inject constructor() {
|
||||
var environment: Environment = Environment.Us
|
||||
set(value) {
|
||||
field = value
|
||||
updateBaseUrls(environment = value)
|
||||
}
|
||||
|
||||
/**
|
||||
* An interceptor for "/api" calls.
|
||||
*/
|
||||
val apiInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
/**
|
||||
* An interceptor for "/identity" calls.
|
||||
*/
|
||||
val identityInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
/**
|
||||
* An interceptor for "/events" calls.
|
||||
*/
|
||||
val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
init {
|
||||
// Ensure all interceptors begin with a default value
|
||||
environment = Environment.Us
|
||||
}
|
||||
|
||||
private fun updateBaseUrls(environment: Environment) {
|
||||
val environmentUrlData = environment.environmentUrlData
|
||||
val baseUrl = environmentUrlData.base.trim()
|
||||
|
||||
// Determine the required base URLs
|
||||
val apiUrl: String
|
||||
val identityUrl: String
|
||||
val eventsUrl: String
|
||||
if (baseUrl.isNotEmpty()) {
|
||||
apiUrl = "$baseUrl/api"
|
||||
identityUrl = "$baseUrl/identity"
|
||||
eventsUrl = "$baseUrl/events"
|
||||
} else {
|
||||
apiUrl =
|
||||
environmentUrlData.api.orNullIfBlank() ?: "https://api.bitwarden.com"
|
||||
identityUrl =
|
||||
environmentUrlData.identity.orNullIfBlank() ?: "https://identity.bitwarden.com"
|
||||
eventsUrl =
|
||||
environmentUrlData.events.orNullIfBlank() ?: "https://events.bitwarden.com"
|
||||
}
|
||||
|
||||
// Update the base URLs
|
||||
apiInterceptor.baseUrl = apiUrl
|
||||
identityInterceptor.baseUrl = identityUrl
|
||||
eventsInterceptor.baseUrl = eventsUrl
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* A collection of various [Retrofit] instances that serve different purposes.
|
||||
*/
|
||||
interface Retrofits {
|
||||
/**
|
||||
* Allows access to "/api" calls that must be authenticated.
|
||||
*
|
||||
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
|
||||
*/
|
||||
val authenticatedApiRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to "/api" calls that do not require authentication.
|
||||
*
|
||||
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
|
||||
*/
|
||||
val unauthenticatedApiRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to "/identity" calls that do not require authentication.
|
||||
*
|
||||
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
|
||||
*/
|
||||
val unauthenticatedIdentityRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to static API calls (ex: external APIs) that do not therefore require
|
||||
* authentication with Bitwarden's servers.
|
||||
*
|
||||
* No base URL is supplied as part of the builder and no longer is added to make this URL
|
||||
* dynamically updatable.
|
||||
*/
|
||||
val staticRetrofitBuilder: Retrofit.Builder
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
*/
|
||||
class RetrofitsImpl(
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
json: Json,
|
||||
) : Retrofits {
|
||||
//region Authenticated Retrofits
|
||||
|
||||
override val authenticatedApiRetrofit: Retrofit by lazy {
|
||||
createAuthenticatedRetrofit(
|
||||
baseUrlInterceptor = baseUrlInterceptors.apiInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
//endregion Authenticated Retrofits
|
||||
|
||||
//region Unauthenticated Retrofits
|
||||
|
||||
override val unauthenticatedApiRetrofit: Retrofit by lazy {
|
||||
createUnauthenticatedRetrofit(
|
||||
baseUrlInterceptor = baseUrlInterceptors.apiInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
override val unauthenticatedIdentityRetrofit: Retrofit by lazy {
|
||||
createUnauthenticatedRetrofit(
|
||||
baseUrlInterceptor = baseUrlInterceptors.identityInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
//endregion Unauthenticated Retrofits
|
||||
|
||||
//region Other Retrofits
|
||||
|
||||
override val staticRetrofitBuilder: Retrofit.Builder by lazy {
|
||||
baseRetrofitBuilder
|
||||
.client(
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
//endregion Other Retrofits
|
||||
|
||||
//region Helper properties and functions
|
||||
private val loggingInterceptor: HttpLoggingInterceptor by lazy {
|
||||
HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
}
|
||||
}
|
||||
|
||||
private val baseOkHttpClient: OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.build()
|
||||
|
||||
private val authenticatedOkHttpClient: OkHttpClient by lazy {
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor(authTokenInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val baseRetrofit: Retrofit by lazy {
|
||||
baseRetrofitBuilder
|
||||
.baseUrl("https://api.bitwarden.com")
|
||||
.build()
|
||||
}
|
||||
|
||||
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
|
||||
Retrofit.Builder()
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.client(baseOkHttpClient)
|
||||
}
|
||||
|
||||
private fun createAuthenticatedRetrofit(
|
||||
baseUrlInterceptor: BaseUrlInterceptor,
|
||||
): Retrofit =
|
||||
baseRetrofit
|
||||
.newBuilder()
|
||||
.client(
|
||||
authenticatedOkHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun createUnauthenticatedRetrofit(
|
||||
baseUrlInterceptor: BaseUrlInterceptor,
|
||||
): Retrofit =
|
||||
baseRetrofit
|
||||
.newBuilder()
|
||||
.client(
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
//endregion Helper properties and functions
|
||||
}
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
@ -15,6 +16,7 @@ class NetworkConfigRepositoryImpl(
|
|||
private val authRepository: AuthRepository,
|
||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val baseUrlInterceptors: BaseUrlInterceptors,
|
||||
dispatcher: CoroutineDispatcher,
|
||||
) : NetworkConfigRepository {
|
||||
|
||||
|
@ -35,7 +37,7 @@ class NetworkConfigRepositoryImpl(
|
|||
environmentRepository
|
||||
.environmentStateFlow
|
||||
.onEach { environment ->
|
||||
// TODO: Update base URL interceptors (BIT-725)
|
||||
baseUrlInterceptors.environment = environment
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository.di
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepository
|
||||
|
@ -37,11 +38,13 @@ object RepositoryModule {
|
|||
authRepository: AuthRepository,
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
): NetworkConfigRepository =
|
||||
NetworkConfigRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
environmentRepository = environmentRepository,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
dispatcher = Dispatchers.IO,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
/**
|
||||
* Returns the original [String] only if:
|
||||
*
|
||||
* - it is non-null
|
||||
* - it is not blank (where blank refers to empty strings of those containing only white space)
|
||||
*
|
||||
* Otherwise `null` is returned.
|
||||
*/
|
||||
fun String?.orNullIfBlank(): String? =
|
||||
this?.takeUnless { it.isBlank() }
|
|
@ -1,14 +1,12 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.network.di
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.NetworkModule
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
|
@ -20,6 +18,7 @@ object NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncApiService(@Named(NetworkModule.AUTHORIZED) retrofit: Retrofit): SyncApi =
|
||||
retrofit.create()
|
||||
fun provideSyncApiService(
|
||||
retrofits: Retrofits,
|
||||
): SyncApi = retrofits.authenticatedApiRetrofit.create()
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
|||
private val identityService = IdentityServiceImpl(
|
||||
api = identityApi,
|
||||
json = Json,
|
||||
baseUrl = server.url("/").toString(),
|
||||
deviceModelProvider = deviceModelProvider,
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
|
||||
|
||||
import okhttp3.Request
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BaseUrlInterceptorTest {
|
||||
private val baseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
@Test
|
||||
fun `intercept with a null base URL should proceed with the original request`() {
|
||||
val request = Request.Builder().url("http://www.fake.com/").build()
|
||||
val chain = FakeInterceptorChain(request)
|
||||
|
||||
val response = baseUrlInterceptor.intercept(chain)
|
||||
|
||||
assertEquals(request, response.request)
|
||||
assertEquals("http", response.request.url.scheme)
|
||||
assertEquals("www.fake.com", response.request.url.host)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept with a non-null base URL should update the base URL used by the request`() {
|
||||
baseUrlInterceptor.baseUrl = "https://api.bitwarden.com"
|
||||
|
||||
val request = Request.Builder().url("http://www.fake.com/").build()
|
||||
val chain = FakeInterceptorChain(request)
|
||||
|
||||
val response = baseUrlInterceptor.intercept(chain)
|
||||
|
||||
assertNotEquals(request, response.request)
|
||||
assertEquals("https", response.request.url.scheme)
|
||||
assertEquals("api.bitwarden.com", response.request.url.host)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import retrofit2.http.GET
|
||||
|
||||
class RetrofitsTest {
|
||||
private val authTokenInterceptor = mockk<AuthTokenInterceptor> {
|
||||
mockIntercept { isAuthInterceptorCalled = true }
|
||||
}
|
||||
private val baseUrlInterceptors = mockk<BaseUrlInterceptors> {
|
||||
every { apiInterceptor } returns mockk {
|
||||
mockIntercept { isApiInterceptorCalled = true }
|
||||
}
|
||||
every { identityInterceptor } returns mockk {
|
||||
mockIntercept { isIdentityInterceptorCalled = true }
|
||||
}
|
||||
every { eventsInterceptor } returns mockk {
|
||||
mockIntercept { isEventsInterceptorCalled = true }
|
||||
}
|
||||
}
|
||||
private val json = Json
|
||||
private val server = MockWebServer()
|
||||
|
||||
private val retrofits = RetrofitsImpl(
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
json = json,
|
||||
)
|
||||
|
||||
private var isAuthInterceptorCalled = false
|
||||
private var isApiInterceptorCalled = false
|
||||
private var isIdentityInterceptorCalled = false
|
||||
private var isEventsInterceptorCalled = false
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
server.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticatedApiRetrofit should invoke the correct interceptors`() = runBlocking {
|
||||
val testApi = retrofits
|
||||
.authenticatedApiRetrofit
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unauthenticatedApiRetrofit should invoke the correct interceptors`() = runBlocking {
|
||||
val testApi = retrofits
|
||||
.unauthenticatedApiRetrofit
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unauthenticatedIdentityRetrofit should invoke the correct interceptors`() = runBlocking {
|
||||
val testApi = retrofits
|
||||
.unauthenticatedIdentityRetrofit
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `staticRetrofitBuilder should invoke the correct interceptors`() = runBlocking {
|
||||
val testApi = retrofits
|
||||
.staticRetrofitBuilder
|
||||
.baseUrl(server.url("/").toString())
|
||||
.build()
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
|
||||
private fun Retrofit.createMockRetrofit(): Retrofit =
|
||||
this
|
||||
.newBuilder()
|
||||
.baseUrl(server.url("/").toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
@GET("/test")
|
||||
suspend fun test(): JsonObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the given [Interceptor] such that the [Interceptor.intercept] is a no-op but triggers the
|
||||
* [isCalledCallback].
|
||||
*/
|
||||
private fun Interceptor.mockIntercept(isCalledCallback: () -> Unit) {
|
||||
val chainSlot = slot<Interceptor.Chain>()
|
||||
every { intercept(capture(chainSlot)) } answers {
|
||||
isCalledCallback()
|
||||
val chain = chainSlot.captured
|
||||
chain.proceed(chain.request())
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -28,6 +29,7 @@ class NetworkConfigRepositoryTest {
|
|||
}
|
||||
|
||||
private val authTokenInterceptor = AuthTokenInterceptor()
|
||||
private val baseUrlInterceptors = BaseUrlInterceptors()
|
||||
|
||||
private lateinit var networkConfigRepository: NetworkConfigRepository
|
||||
|
||||
|
@ -37,6 +39,7 @@ class NetworkConfigRepositoryTest {
|
|||
authRepository = authRepository,
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
environmentRepository = environmentRepository,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
dispatcher = UnconfinedTestDispatcher(),
|
||||
)
|
||||
}
|
||||
|
@ -55,4 +58,19 @@ class NetworkConfigRepositoryTest {
|
|||
mutableAuthStateFlow.value = AuthState.Unauthenticated
|
||||
assertNull(authTokenInterceptor.authToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changes in the Environment should update the BaseUrlInterceptors`() {
|
||||
mutableEnvironmentStateFlow.value = Environment.Us
|
||||
assertEquals(
|
||||
Environment.Us,
|
||||
baseUrlInterceptors.environment,
|
||||
)
|
||||
|
||||
mutableEnvironmentStateFlow.value = Environment.Eu
|
||||
assertEquals(
|
||||
Environment.Eu,
|
||||
baseUrlInterceptors.environment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class StringExtensionsTest {
|
||||
@Test
|
||||
fun `orNullIfBlank returns null for a null String`() {
|
||||
assertNull((null as String?).orNullIfBlank())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `orNullIfBlank returns null for an empty String`() {
|
||||
assertNull("".orNullIfBlank())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `orNullIfBlank returns null for a blank String`() {
|
||||
assertNull(" ".orNullIfBlank())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `orNullIfBlank returns the original value for a non-blank String`() {
|
||||
assertEquals(
|
||||
"test",
|
||||
"test".orNullIfBlank(),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue