[PM-19820] Replace ResultCallAdapterFactory in authenticator module (#4968)

This commit is contained in:
Patrick Honkonen
2025-04-02 14:48:45 -04:00
committed by GitHub
parent ce139623d6
commit 20bda929b3
10 changed files with 11 additions and 204 deletions

View File

@@ -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)

View File

@@ -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>
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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>
}

View File

@@ -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>
}
/**