use KotlinResultCallAdapter instead of NetworkCallAdapter (#13)

This commit is contained in:
Konrad Pozniak 2022-01-31 09:04:55 +01:00 committed by GitHub
parent 466dba6096
commit a27035b84d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 50 additions and 740 deletions

View File

@ -65,6 +65,8 @@ dependencies {
val daggerVersion = "2.40.5"
val jUnitVersion = "5.8.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.activity:activity-ktx:1.4.0")
@ -98,7 +100,7 @@ dependencies {
implementation("com.squareup.moshi:moshi-adapters:$moshiVersion")
kapt("com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("at.connyduck:kotlin-result-calladapter:1.0.0")
implementation("com.fxn769:pix:1.5.6")

View File

@ -36,7 +36,6 @@ import at.connyduck.pixelcat.components.util.getMimeType
import at.connyduck.pixelcat.db.AccountManager
import at.connyduck.pixelcat.model.NewStatus
import at.connyduck.pixelcat.network.FediverseApi
import at.connyduck.pixelcat.network.calladapter.NetworkResponseError
import dagger.android.DaggerService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -49,6 +48,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.io.IOException
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
@ -173,7 +173,7 @@ class SendStatusService : DaggerService(), CoroutineScope {
account.domain,
statusToSend.idempotencyKey,
newStatus
).fold<Any?>(
).fold(
{
statusesToSend.remove(id)
stopSelfWhenDone()
@ -192,20 +192,8 @@ class SendStatusService : DaggerService(), CoroutineScope {
val statusToSend = statusesToSend[id] ?: return
when (error) {
is NetworkResponseError.ApiError, is UnrecoverableError -> {
// the server refused to accept the status, save toot & show error message
// TODO saveToDrafts
val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_cat)
.setContentTitle(getString(R.string.send_status_notification_error_title))
// .setContentText(getString(R.string.send_toot_notification_saved_content))
.setColor(getColorForAttr(android.R.attr.colorPrimary))
notificationManager.cancel(id)
notificationManager.notify(errorNotificationId--, builder.build())
}
else -> {
is IOException -> {
// possibly a network problem, we might still have a chance sending the status
var backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong())
if (backoff > MAX_RETRY_INTERVAL) {
backoff = MAX_RETRY_INTERVAL
@ -220,6 +208,19 @@ class SendStatusService : DaggerService(), CoroutineScope {
backoff
)
}
else -> {
// the server refused to accept the status, save toot & show error message
// TODO saveToDrafts
val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_cat)
.setContentTitle(getString(R.string.send_status_notification_error_title))
// .setContentText(getString(R.string.send_toot_notification_saved_content))
.setColor(getColorForAttr(android.R.attr.colorPrimary))
notificationManager.cancel(id)
notificationManager.notify(errorNotificationId--, builder.build())
}
}
}

View File

@ -6,7 +6,6 @@ import at.connyduck.pixelcat.db.entitity.StatusEntity
import at.connyduck.pixelcat.db.entitity.toEntity
import at.connyduck.pixelcat.model.Status
import at.connyduck.pixelcat.network.FediverseApi
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
import javax.inject.Inject
import javax.inject.Singleton
@ -43,8 +42,8 @@ class TimelineUseCases @Inject constructor(
)
}
private suspend fun NetworkResponse<Status>.updateStatusInDb() {
fold<Any?>(
private suspend fun Result<Status>.updateStatusInDb() {
fold(
{ updatedStatus ->
val accountId = accountManager.activeAccount()?.id!!
val updatedStatusEntity = updatedStatus.toEntity(accountId)

View File

@ -19,6 +19,7 @@
package at.connyduck.pixelcat.dagger
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
import at.connyduck.pixelcat.BuildConfig
import at.connyduck.pixelcat.db.AccountManager
import at.connyduck.pixelcat.model.Notification
@ -26,7 +27,6 @@ import at.connyduck.pixelcat.network.FediverseApi
import at.connyduck.pixelcat.network.InstanceSwitchAuthInterceptor
import at.connyduck.pixelcat.network.RefreshTokenAuthenticator
import at.connyduck.pixelcat.network.UserAgentInterceptor
import at.connyduck.pixelcat.network.calladapter.NetworkResponseAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.EnumJsonAdapter
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
@ -82,7 +82,7 @@ class NetworkModule {
return Retrofit.Builder()
.baseUrl("https://" + FediverseApi.PLACEHOLDER_DOMAIN)
.client(httpClient)
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}

View File

@ -28,7 +28,6 @@ import at.connyduck.pixelcat.model.Notification
import at.connyduck.pixelcat.model.Relationship
import at.connyduck.pixelcat.model.Status
import at.connyduck.pixelcat.model.StatusContext
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.Field
@ -57,7 +56,7 @@ interface FediverseApi {
@Field("website") clientWebsite: String,
@Field("redirect_uris") redirectUris: String,
@Field("scopes") scopes: String
): NetworkResponse<AppCredentials>
): Result<AppCredentials>
@FormUrlEncoded
@POST("oauth/token")
@ -68,7 +67,7 @@ interface FediverseApi {
@Field("redirect_uri") redirectUri: String,
@Field("code") code: String,
@Field("grant_type") grantType: String = "authorization_code"
): NetworkResponse<AccessToken>
): Result<AccessToken>
@FormUrlEncoded
@POST("oauth/token")
@ -78,17 +77,17 @@ interface FediverseApi {
@Field("client_secret") clientSecret: String,
@Field("refresh_token") refreshToken: String,
@Field("grant_type") grantType: String = "refresh_token"
): NetworkResponse<AccessToken>
): Result<AccessToken>
@GET("api/v1/accounts/verify_credentials")
suspend fun accountVerifyCredentials(): NetworkResponse<Account>
suspend fun accountVerifyCredentials(): Result<Account>
@GET("api/v1/timelines/home")
suspend fun homeTimeline(
@Query("max_id") maxId: String? = null,
@Query("since_id") sinceId: String? = null,
@Query("limit") limit: Int? = null
): NetworkResponse<List<Status>>
): Result<List<Status>>
@GET("api/v1/accounts/{id}/statuses")
suspend fun accountTimeline(
@ -99,12 +98,12 @@ interface FediverseApi {
@Query("exclude_replies") excludeReplies: Boolean? = false,
@Query("only_media") onlyMedia: Boolean? = true,
@Query("pinned") pinned: Boolean? = false
): NetworkResponse<Status>
): Result<Status>
@GET("api/v1/accounts/{id}")
suspend fun account(
@Path("id") accountId: String
): NetworkResponse<Account>
): Result<Account>
@GET("api/v1/accounts/{id}/statuses")
suspend fun accountStatuses(
@ -114,50 +113,50 @@ interface FediverseApi {
@Query("limit") limit: Int? = null,
@Query("only_media") onlyMedia: Boolean? = null,
@Query("exclude_reblogs") excludeReblogs: Boolean? = null
): NetworkResponse<List<Status>>
): Result<List<Status>>
@FormUrlEncoded
@POST("api/v1/accounts/{id}/follow")
suspend fun followAccount(
@Path("id") accountId: String,
@Field("reblogs") showReblogs: Boolean
): NetworkResponse<Relationship>
): Result<Relationship>
@POST("api/v1/accounts/{id}/unfollow")
suspend fun unfollowAccount(
@Path("id") accountId: String
): NetworkResponse<Relationship>
): Result<Relationship>
@POST("api/v1/accounts/{id}/block")
suspend fun blockAccount(
@Path("id") accountId: String
): NetworkResponse<Relationship>
): Result<Relationship>
@POST("api/v1/accounts/{id}/unblock")
suspend fun unblockAccount(
@Path("id") accountId: String
): NetworkResponse<Relationship>
): Result<Relationship>
@POST("api/v1/accounts/{id}/mute")
suspend fun muteAccount(
@Path("id") accountId: String
): NetworkResponse<Relationship>
): Result<Relationship>
@POST("api/v1/accounts/{id}/unmute")
suspend fun unmuteAccount(
@Path("id") accountId: String
): NetworkResponse<Relationship>
): Result<Relationship>
@GET("api/v1/accounts/relationships")
suspend fun relationships(
@Query("id[]") accountIds: List<String>
): NetworkResponse<List<Relationship>>
): Result<List<Relationship>>
@Multipart
@POST("api/v1/media")
suspend fun uploadMedia(
@Part file: MultipartBody.Part
): NetworkResponse<Attachment>
): Result<Attachment>
@POST("api/v1/statuses")
suspend fun createStatus(
@ -165,42 +164,42 @@ interface FediverseApi {
@Header(DOMAIN_HEADER) domain: String,
@Header("Idempotency-Key") idempotencyKey: String,
@Body status: NewStatus
): NetworkResponse<Status>
): Result<Status>
@POST("api/v1/statuses")
suspend fun reply(
@Body status: NewStatus
): NetworkResponse<Status>
): Result<Status>
@POST("api/v1/statuses/{id}/favourite")
suspend fun favouriteStatus(
@Path("id") statusId: String
): NetworkResponse<Status>
): Result<Status>
@POST("api/v1/statuses/{id}/unfavourite")
suspend fun unfavouriteStatus(
@Path("id") statusId: String
): NetworkResponse<Status>
): Result<Status>
@POST("api/v1/statuses/{id}/reblog")
suspend fun reblogStatus(
@Path("id") statusId: String
): NetworkResponse<Status>
): Result<Status>
@POST("api/v1/statuses/{id}/unreblog")
suspend fun unreblogStatus(
@Path("id") statusId: String
): NetworkResponse<Status>
): Result<Status>
@GET("api/v1/statuses/{id}")
suspend fun status(
@Path("id") statusId: String
): NetworkResponse<Status>
): Result<Status>
@GET("api/v1/statuses/{id}/context")
suspend fun statusContext(
@Path("id") statusId: String
): NetworkResponse<StatusContext>
): Result<StatusContext>
@GET("api/v1/notifications")
suspend fun notifications(
@ -208,5 +207,5 @@ interface FediverseApi {
@Query("since_id") sinceId: String? = null,
@Query("limit") limit: Int? = null,
@Query("exclude_types[]") excludes: Set<String>? = null
): NetworkResponse<List<Notification>>
): Result<List<Notification>>
}

View File

@ -1,35 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type
class NetworkCallAdapter<S : Any>(
private val successType: Type
) : CallAdapter<S, Call<NetworkResponse<S>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<S>): Call<NetworkResponse<S>> {
return NetworkResponseCall(call)
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import java.io.IOException
sealed class NetworkResponse<out A : Any> {
data class Success<T : Any>(val body: T) : NetworkResponse<T>()
data class Failure(val reason: NetworkResponseError) : NetworkResponse<Nothing>()
inline fun <B> fold(onSuccess: (A) -> B, onFailure: (NetworkResponseError) -> B): B = when (this) {
is Success -> onSuccess(body)
is Failure -> onFailure(reason)
}
}
sealed class NetworkResponseError : Throwable() {
data class ApiError(val code: Int) : NetworkResponseError()
/**
* Network error
*/
data class NetworkError(val error: IOException) : NetworkResponseError()
/**
* For example, json parsing error
*/
data class UnknownError(val error: Throwable?) : NetworkResponseError()
}

View File

@ -1,60 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
class NetworkResponseAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// suspend functions wrap the response type in `Call`
if (Call::class.java != getRawType(returnType)) {
return null
}
// check first that the return type is `ParameterizedType`
check(returnType is ParameterizedType) {
"return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
}
// get the response type inside the `Call` type
val responseType = getParameterUpperBound(0, returnType)
// if the response type is not ApiResponse then we can't handle this type, so we return null
if (getRawType(responseType) != NetworkResponse::class.java) {
return null
}
// the response type is ApiResponse and should be parameterized
check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>" }
val successBodyType = getParameterUpperBound(0, responseType)
return NetworkCallAdapter<Any>(successBodyType)
}
}

View File

@ -1,102 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
internal class NetworkResponseCall<S : Any>(
private val delegate: Call<S>
) : Call<NetworkResponse<S>> {
override fun enqueue(callback: Callback<NetworkResponse<S>>) {
return delegate.enqueue(
object : Callback<S> {
override fun onResponse(call: Call<S>, response: Response<S>) {
val body = response.body()
if (response.isSuccessful) {
if (body != null) {
callback.onResponse(
this@NetworkResponseCall,
Response.success(NetworkResponse.Success(body))
)
} else {
// Response is successful but the body is null
callback.onResponse(
this@NetworkResponseCall,
Response.success(
NetworkResponse.Failure(
NetworkResponseError.ApiError(
response.code()
)
)
)
)
}
} else {
callback.onResponse(
this@NetworkResponseCall,
Response.success(
NetworkResponse.Failure(
NetworkResponseError.ApiError(
response.code()
)
)
)
)
}
}
override fun onFailure(call: Call<S>, throwable: Throwable) {
val networkResponse = when (throwable) {
is IOException -> NetworkResponse.Failure(
NetworkResponseError.NetworkError(
throwable
)
)
else -> NetworkResponse.Failure(NetworkResponseError.UnknownError(throwable))
}
callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
}
}
)
}
override fun isExecuted() = delegate.isExecuted
override fun clone() = NetworkResponseCall(delegate.clone())
override fun isCanceled() = delegate.isCanceled
override fun cancel() = delegate.cancel()
override fun execute(): Response<NetworkResponse<S>> {
throw UnsupportedOperationException("NetworkResponseCall doesn't support synchronized execution")
}
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
}

View File

@ -1,130 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.SocketPolicy
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.io.IOException
class ApiTest {
private var mockWebServer = MockWebServer()
private lateinit var api: TestApi
@BeforeEach
fun setup() {
mockWebServer.start()
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
api = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(OkHttpClient())
.build()
.create(TestApi::class.java)
}
@AfterEach
fun shutdown() {
mockWebServer.shutdown()
}
private fun mockResponse(responseCode: Int, body: String = "") = MockResponse()
.setResponseCode(responseCode)
.setBody(body)
@Test
fun `should return the correct test object`() {
val response = mockResponse(
200,
"""
{
"lets": "not",
"test": 1
}
"""
)
mockWebServer.enqueue(response)
val responseObject = runBlocking {
api.testEndpoint()
}
assertEquals(
NetworkResponse.Success(TestResponseClass("not", 1)),
responseObject
)
}
@Test
fun `should return a ApiError failure when the server returns error 500`() {
val errorCode = 500
val response = mockResponse(errorCode)
mockWebServer.enqueue(response)
val responseObject = runBlocking {
api.testEndpoint()
}
assertEquals(
NetworkResponse.Failure(NetworkResponseError.ApiError(errorCode)),
responseObject
)
}
@Test
fun `should return a NetworkError failure when the network fails`() {
mockWebServer.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.DISCONNECT_AFTER_REQUEST })
val responseObject = runBlocking {
api.testEndpoint()
}
assertEquals(
NetworkResponse.Failure(
NetworkResponseError.NetworkError(
object : IOException() {
override fun equals(other: Any?): Boolean {
return (other is IOException)
}
}
)
),
responseObject
)
}
}

View File

@ -1,51 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import com.squareup.moshi.Types
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import retrofit2.Call
import retrofit2.Retrofit
class NetworkResponseAdapterFactoryTest {
private val retrofit = Retrofit.Builder().baseUrl("http://example.com").build()
@Test
fun `should return a NetworkResponseCallAdapter when the type is supported`() {
val networkResponseType =
Types.newParameterizedType(NetworkResponse::class.java, TestResponseClass::class.java)
val callType = Types.newParameterizedType(Call::class.java, networkResponseType)
val adapter = NetworkResponseAdapterFactory().get(callType, arrayOf(), retrofit)
assertEquals(TestResponseClass::class.java, adapter?.responseType())
}
@Test
fun `should return null if the type is not supported`() {
val adapter = NetworkResponseAdapterFactory().get(TestResponseClass::class.java, arrayOf(), retrofit)
assertNull(adapter)
}
}

View File

@ -1,137 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
class NetworkResponseCallTest {
private val backingCall = TestCall<String>()
private val networkCall = NetworkResponseCall(backingCall)
@Test
fun `should throw an error when invoking 'execute'`() {
assertThrows<UnsupportedOperationException> {
networkCall.execute()
}
}
@Test
fun `should delegate properties to backing call`() {
with(networkCall) {
assertEquals(isExecuted, backingCall.isExecuted)
assertEquals(isCanceled, backingCall.isCanceled)
assertEquals(request(), backingCall.request())
}
}
@Test
fun `should return new instance when cloned`() {
val clonedCall = networkCall.clone()
assert(clonedCall !== networkCall)
}
@Test
fun `should cancel backing call as well when cancelled`() {
networkCall.cancel()
assert(backingCall.isCanceled)
}
@Test
fun `should parse successful call as NetworkResponse Success`() {
val body = "Test body"
networkCall.enqueue(
object : Callback<NetworkResponse<String>> {
override fun onResponse(
call: Call<NetworkResponse<String>>,
response: Response<NetworkResponse<String>>
) {
assertTrue(response.isSuccessful)
assertEquals(
response.body(),
NetworkResponse.Success(body)
)
}
override fun onFailure(call: Call<NetworkResponse<String>>, t: Throwable) {
throw IllegalStateException()
}
}
)
backingCall.complete(body)
}
@Test
fun `should parse call with 404 error code as ApiError`() {
val errorCode = 404
val errorBody = "not found"
networkCall.enqueue(
object : Callback<NetworkResponse<String>> {
override fun onResponse(
call: Call<NetworkResponse<String>>,
response: Response<NetworkResponse<String>>
) {
assertEquals(
response.body(),
NetworkResponse.Failure(NetworkResponseError.ApiError(errorCode))
)
}
override fun onFailure(call: Call<NetworkResponse<String>>, t: Throwable) {
throw IllegalStateException()
}
}
)
backingCall.complete(Response.error(errorCode, errorBody.toResponseBody()))
}
@Test
fun `should parse call with IOException as NetworkError`() {
val exception = IOException()
networkCall.enqueue(
object : Callback<NetworkResponse<String>> {
override fun onResponse(
call: Call<NetworkResponse<String>>,
response: Response<NetworkResponse<String>>
) {
assertEquals(
response.body(),
NetworkResponse.Failure(NetworkResponseError.NetworkError(exception))
)
}
override fun onFailure(call: Call<NetworkResponse<String>>, t: Throwable) {
throw IllegalStateException()
}
}
)
backingCall.completeWithException(exception)
}
}

View File

@ -1,28 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import retrofit2.http.GET
interface TestApi {
@GET("testpath")
suspend fun testEndpoint(): NetworkResponse<TestResponseClass>
}

View File

@ -1,75 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.InterruptedIOException
class TestCall<T> : Call<T> {
private var executed = false
private var canceled = false
private var callback: Callback<T>? = null
private var request = Request.Builder().url("http://example.com").build()
fun completeWithException(t: Throwable) {
synchronized(this) {
callback?.onFailure(this, t)
}
}
fun complete(body: T) = complete(Response.success(body))
fun complete(response: Response<T>) {
synchronized(this) {
callback?.onResponse(this, response)
}
}
override fun enqueue(callback: Callback<T>) {
synchronized(this) {
this.callback = callback
}
}
override fun isExecuted() = synchronized(this) { executed }
override fun isCanceled() = synchronized(this) { canceled }
override fun clone() = TestCall<T>()
override fun cancel() {
synchronized(this) {
if (canceled) return
canceled = true
val exception = InterruptedIOException("canceled")
callback?.onFailure(this, exception)
}
}
override fun execute(): Response<T> {
throw UnsupportedOperationException("Network call does not support synchronous execution")
}
override fun request() = request
override fun timeout() = Timeout()
}

View File

@ -1,25 +0,0 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.network.calladapter
data class TestResponseClass(
val lets: String,
val test: Int
)

View File

@ -14,6 +14,7 @@ buildscript {
allprojects {
repositories {
google()
mavenCentral()
jcenter()
maven(url = "https://jitpack.io")
}