mirror of
https://github.com/bitwarden/android.git
synced 2026-03-09 03:33:36 -05:00
[PM-19820] Replace ResultCallAdapterFactory in authenticator module (#4968)
This commit is contained in:
@@ -152,6 +152,7 @@ dependencies {
|
||||
implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
|
||||
|
||||
implementation(project(":core"))
|
||||
implementation(project(":network"))
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.api
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import retrofit2.http.GET
|
||||
|
||||
/**
|
||||
@@ -9,5 +10,5 @@ import retrofit2.http.GET
|
||||
interface ConfigApi {
|
||||
|
||||
@GET("config")
|
||||
suspend fun getConfig(): Result<ConfigResponseJson>
|
||||
suspend fun getConfig(): NetworkResult<ConfigResponseJson>
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.core
|
||||
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import okhttp3.Request
|
||||
import okio.IOException
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* The integer code value for a "No Content" response.
|
||||
*/
|
||||
private const val NO_CONTENT_RESPONSE_CODE: Int = 204
|
||||
|
||||
/**
|
||||
* A [Call] for wrapping a network request into a [Result].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class ResultCall<T>(
|
||||
private val backingCall: Call<T>,
|
||||
private val successType: Type,
|
||||
) : Call<Result<T>> {
|
||||
override fun cancel(): Unit = backingCall.cancel()
|
||||
|
||||
override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)
|
||||
|
||||
override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
|
||||
object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
callback.onResponse(this@ResultCall, Response.success(response.toResult()))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun execute(): Response<Result<T>> =
|
||||
try {
|
||||
Response.success(
|
||||
backingCall
|
||||
.execute()
|
||||
.toResult(),
|
||||
)
|
||||
} catch (ioException: IOException) {
|
||||
Response.success(ioException.toFailure())
|
||||
} catch (runtimeException: RuntimeException) {
|
||||
Response.success(runtimeException.toFailure())
|
||||
}
|
||||
|
||||
override fun isCanceled(): Boolean = backingCall.isCanceled
|
||||
|
||||
override fun isExecuted(): Boolean = backingCall.isExecuted
|
||||
|
||||
override fun request(): Request = backingCall.request()
|
||||
|
||||
override fun timeout(): Timeout = backingCall.timeout()
|
||||
|
||||
/**
|
||||
* Synchronously send the request and return its response as a [Result].
|
||||
*/
|
||||
fun executeForResult(): Result<T> = requireNotNull(execute().body())
|
||||
|
||||
private fun Throwable.toFailure(): Result<T> =
|
||||
this
|
||||
.also {
|
||||
// We rebuild the URL without query params, we do not want to log those
|
||||
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
|
||||
}
|
||||
.asFailure()
|
||||
|
||||
private fun Response<T>.toResult(): Result<T> =
|
||||
if (!this.isSuccessful) {
|
||||
HttpException(this).toFailure()
|
||||
} else {
|
||||
val body = this.body()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when {
|
||||
// We got a nonnull T as the body, just return it.
|
||||
body != null -> body.asSuccess()
|
||||
// We expected the body to be null since the successType is Unit, just return Unit.
|
||||
successType == Unit::class.java -> (Unit as T).asSuccess()
|
||||
// We allow null for 204's, just return null.
|
||||
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
|
||||
// All other null bodies result in an error.
|
||||
else -> IllegalStateException("Unexpected null body!").toFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.core
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A [CallAdapter] for wrapping network requests into [kotlin.Result].
|
||||
*/
|
||||
class ResultCallAdapter<T>(
|
||||
private val successType: Type,
|
||||
) : CallAdapter<T, Call<Result<T>>> {
|
||||
|
||||
override fun responseType(): Type = successType
|
||||
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.core
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result].
|
||||
*/
|
||||
class ResultCallAdapterFactory : CallAdapter.Factory() {
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit,
|
||||
): CallAdapter<*, *>? {
|
||||
check(returnType is ParameterizedType) { "$returnType must be parameterized" }
|
||||
val containerType = getParameterUpperBound(0, returnType)
|
||||
|
||||
if (getRawType(containerType) != Result::class.java) return null
|
||||
check(containerType is ParameterizedType) { "$containerType must be parameterized" }
|
||||
|
||||
val requestType = getParameterUpperBound(0, containerType)
|
||||
|
||||
return if (getRawType(returnType) == Call::class.java) {
|
||||
ResultCallAdapter<Any>(successType = requestType)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptor
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.core.NetworkResultCallAdapterFactory
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -60,7 +60,7 @@ class RetrofitsImpl(
|
||||
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
|
||||
Retrofit.Builder()
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
|
||||
.client(baseOkHttpClient)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ package com.bitwarden.authenticator.data.platform.datasource.network.service
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.api.ConfigApi
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
|
||||
import com.bitwarden.network.util.toResult
|
||||
|
||||
/**
|
||||
* Default implementation of [ConfigService] for querying for app configurations.
|
||||
*/
|
||||
class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService {
|
||||
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig()
|
||||
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig().toResult()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.bitwarden.authenticator.data.platform.base
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.bitwarden.core.di.CoreModule
|
||||
import com.bitwarden.network.core.NetworkResultCallAdapterFactory
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
@@ -24,7 +24,7 @@ abstract class BaseServiceTest {
|
||||
|
||||
protected val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl(url.toString())
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.core
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import retrofit2.http.GET
|
||||
|
||||
class ResultCallAdapterTest {
|
||||
|
||||
private val server: MockWebServer = MockWebServer().apply { start() }
|
||||
private val testService: FakeService =
|
||||
Retrofit
|
||||
.Builder()
|
||||
.baseUrl(server.url("/").toString())
|
||||
// add the adapter being tested
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.build()
|
||||
.create()
|
||||
|
||||
@AfterEach
|
||||
fun after() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server returns error response code result should be failure`() = runBlocking {
|
||||
server.enqueue(MockResponse().setResponseCode(500))
|
||||
val result = testService.requestWithUnitData()
|
||||
Assertions.assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server returns successful response result should be success`() = runBlocking {
|
||||
server.enqueue(MockResponse())
|
||||
val result = testService.requestWithUnitData()
|
||||
Assertions.assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake retrofit service used for testing call adapters.
|
||||
*/
|
||||
private interface FakeService {
|
||||
@GET("/fake")
|
||||
suspend fun requestWithUnitData(): Result<Unit>
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
@@ -109,7 +110,7 @@ class RetrofitsTest {
|
||||
|
||||
interface TestApi {
|
||||
@GET("/test")
|
||||
suspend fun test(): Result<JsonObject>
|
||||
suspend fun test(): NetworkResult<JsonObject>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user