diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt index c07a13f7d..2210f002b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/NetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/NetworkModule.kt index 4deb278d4..4b451ffdf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/NetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/NetworkModule.kt @@ -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(), diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt index 0aae1893c..edf740bd1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt @@ -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 = api .getToken( - // TODO: use correct base URL here BIT-328 - url = "$baseUrl/identity/connect/token", scope = "api+offline_access", clientId = "mobile", authEmail = email.base64UrlEncode(), diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt index e145adfb9..2f987acd3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptor.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptor.kt new file mode 100644 index 000000000..6c2f61ab6 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptor.kt @@ -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() diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptors.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptors.kt new file mode 100644 index 000000000..fe77b58de --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptors.kt @@ -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 + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/Retrofits.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/Retrofits.kt new file mode 100644 index 000000000..dca89b7c6 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/Retrofits.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt new file mode 100644 index 000000000..2926b581a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt index 398760e62..4ae1695a1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt @@ -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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt index aa4034202..855766a97 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/StringExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/StringExtensions.kt new file mode 100644 index 000000000..c9cc32acf --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/StringExtensions.kt @@ -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() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/NetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/NetworkModule.kt index c7f34921e..cc17eee1b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/NetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/NetworkModule.kt @@ -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() } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt index aea0da600..f8754cc5f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt @@ -29,7 +29,6 @@ class IdentityServiceTest : BaseServiceTest() { private val identityService = IdentityServiceImpl( api = identityApi, json = Json, - baseUrl = server.url("/").toString(), deviceModelProvider = deviceModelProvider, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptorTest.kt new file mode 100644 index 000000000..172fdd7b0 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptorTest.kt @@ -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) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptorsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptorsTest.kt new file mode 100644 index 000000000..cbe0b79c1 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/BaseUrlInterceptorsTest.kt @@ -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, + ) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt new file mode 100644 index 000000000..cfcde1209 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt @@ -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 { + mockIntercept { isAuthInterceptorCalled = true } + } + private val baseUrlInterceptors = mockk { + 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() + + 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() + + 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() + + 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() + + 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() + every { intercept(capture(chainSlot)) } answers { + isCalledCallback() + val chain = chainSlot.captured + chain.proceed(chain.request()) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt index eb7335e39..1a93b964b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt @@ -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, + ) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/StringExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/StringExtensionsTest.kt new file mode 100644 index 000000000..f2aff5f80 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/StringExtensionsTest.kt @@ -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(), + ) + } +}