introduce KotlinResultCallAdapter for nice suspending network calls (#2415)

* introduce KotlinResultCallAdapter for nice suspending network calls

* fix tests
This commit is contained in:
Konrad Pozniak 2022-04-14 19:49:49 +02:00 committed by GitHub
parent d21d045eda
commit 3e8c6a318a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 168 additions and 179 deletions

View file

@ -137,6 +137,7 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
implementation "at.connyduck:kotlin-result-calladapter:1.0.0"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
@ -176,8 +177,8 @@ dependencies {
testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "org.robolectric:robolectric:4.4" testImplementation "org.robolectric:robolectric:4.4"
testImplementation "org.mockito:mockito-inline:3.6.28" testImplementation "org.mockito:mockito-inline:4.4.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.room:room-testing:$roomVersion"

View file

@ -682,18 +682,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
} }
private fun fetchUserInfo() { private fun fetchUserInfo() = lifecycleScope.launch {
mastodonApi.accountVerifyCredentials() mastodonApi.accountVerifyCredentials().fold(
.observeOn(AndroidSchedulers.mainThread()) { userInfo ->
.autoDispose(this, Lifecycle.Event.ON_DESTROY) onFetchUserInfoSuccess(userInfo)
.subscribe( },
{ userInfo -> { throwable ->
onFetchUserInfoSuccess(userInfo) Log.e(TAG, "Failed to fetch user info. " + throwable.message)
}, }
{ throwable -> )
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
}
)
} }
private fun onFetchUserInfoSuccess(me: Account) { private fun onFetchUserInfoSuccess(me: Account) {

View file

@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.rx3.rxSingle
import javax.inject.Inject import javax.inject.Inject
class AnnouncementsViewModel @Inject constructor( class AnnouncementsViewModel @Inject constructor(
@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor(
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map<Either<InstanceEntity, Instance>> { Either.Left(it) } .map<Either<InstanceEntity, Instance>> { Either.Left(it) }
.onErrorResumeNext { .onErrorResumeNext {
mastodonApi.getInstance() rxSingle {
.map { Either.Right(it) } mastodonApi.getInstance().getOrThrow()
}.map { Either.Right(it) }
} }
) { emojis, either -> ) { emojis, either ->
either.asLeftOrNull()?.copy(emojiList = emojis) either.asLeftOrNull()?.copy(emojiList = emojis)

View file

@ -48,6 +48,7 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.rxSingle
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -105,7 +106,10 @@ class ComposeViewModel @Inject constructor(
init { init {
Single.zip( Single.zip(
api.getCustomEmojis(), api.getInstance() api.getCustomEmojis(),
rxSingle {
api.getInstance().getOrThrow()
}
) { emojis, instance -> ) { emojis, instance ->
InstanceEntity( InstanceEntity(
instance = accountManager.activeAccount?.domain!!, instance = accountManager.activeAccount?.domain!!,
@ -291,7 +295,7 @@ class ComposeViewModel @Inject constructor(
): LiveData<Unit> { ): LiveData<Unit> {
val deletionObservable = if (isEditingScheduledToot) { val deletionObservable = if (isEditingScheduledToot) {
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
} else { } else {
Observable.just(Unit) Observable.just(Unit)
}.toLiveData() }.toLiveData()

View file

@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ActivityLoginBinding import com.keylesspalace.tusky.databinding.ActivityLoginBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.rickRoll import com.keylesspalace.tusky.util.rickRoll
@ -166,32 +165,33 @@ class LoginActivity : BaseActivity(), Injectable {
setLoading(true) setLoading(true)
lifecycleScope.launch { lifecycleScope.launch {
val credentials: AppCredentials = try { mastodonApi.authenticateApp(
mastodonApi.authenticateApp( domain, getString(R.string.app_name), oauthRedirectUri,
domain, getString(R.string.app_name), oauthRedirectUri, OAUTH_SCOPES, getString(R.string.tusky_website)
OAUTH_SCOPES, getString(R.string.tusky_website) ).fold(
) { credentials ->
} catch (e: Exception) { // Before we open browser page we save the data.
binding.loginButton.isEnabled = true // Even if we don't open other apps user may go to password manager or somewhere else
binding.domainTextInputLayout.error = // and we will need to pick up the process where we left off.
getString(R.string.error_failed_app_registration) // Alternatively we could pass it all as part of the intent and receive it back
setLoading(false) // but it is a bit of a workaround.
Log.e(TAG, Log.getStackTraceString(e)) preferences.edit()
return@launch .putString(DOMAIN, domain)
} .putString(CLIENT_ID, credentials.clientId)
.putString(CLIENT_SECRET, credentials.clientSecret)
.apply()
// Before we open browser page we save the data. redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
// Even if we don't open other apps user may go to password manager or somewhere else },
// and we will need to pick up the process where we left off. { e ->
// Alternatively we could pass it all as part of the intent and receive it back binding.loginButton.isEnabled = true
// but it is a bit of a workaround. binding.domainTextInputLayout.error =
preferences.edit() getString(R.string.error_failed_app_registration)
.putString(DOMAIN, domain) setLoading(false)
.putString(CLIENT_ID, credentials.clientId) Log.e(TAG, Log.getStackTraceString(e))
.putString(CLIENT_SECRET, credentials.clientSecret) return@launch
.apply() }
)
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
} }
} }
@ -224,29 +224,28 @@ class LoginActivity : BaseActivity(), Injectable {
setLoading(true) setLoading(true)
val accessToken = try { mastodonApi.fetchOAuthToken(
mastodonApi.fetchOAuthToken( domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
domain, clientId, clientSecret, oauthRedirectUri, code, ).fold(
"authorization_code" { accessToken ->
) accountManager.addAccount(accessToken.accessToken, domain)
} catch (e: Exception) {
setLoading(false)
binding.domainTextInputLayout.error =
getString(R.string.error_retrieving_oauth_token)
Log.e(
TAG,
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
)
return
}
accountManager.addAccount(accessToken.accessToken, domain) val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val intent = Intent(this, MainActivity::class.java) startActivity(intent)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK finish()
startActivity(intent) overridePendingTransition(R.anim.explode, R.anim.explode)
finish() },
overridePendingTransition(R.anim.explode, R.anim.explode) { e ->
setLoading(false)
binding.domainTextInputLayout.error =
getString(R.string.error_retrieving_oauth_token)
Log.e(
TAG,
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
)
}
)
} }
private fun setLoading(loadingState: Boolean) { private fun setLoading(loadingState: Boolean) {

View file

@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import javax.inject.Inject import javax.inject.Inject
class ScheduledStatusViewModel @Inject constructor( class ScheduledStatusViewModel @Inject constructor(
@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor(
fun deleteScheduledStatus(status: ScheduledStatus) { fun deleteScheduledStatus(status: ScheduledStatus) {
viewModelScope.launch { viewModelScope.launch {
try { mastodonApi.deleteScheduledStatus(status.id).fold(
mastodonApi.deleteScheduledStatus(status.id).await() {
pagingSourceFactory.remove(status) pagingSourceFactory.remove(status)
} catch (throwable: Throwable) { },
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) { throwable ->
} Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
}
)
} }
} }
} }

View file

@ -19,6 +19,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.text.Spanned import android.text.Spanned
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.BuildConfig
@ -111,6 +112,7 @@ class NetworkModule {
.client(httpClient) .client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson)) .addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create()) .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
.build() .build()
} }

View file

@ -80,7 +80,7 @@ interface MastodonApi {
fun getCustomEmojis(): Single<List<Emoji>> fun getCustomEmojis(): Single<List<Emoji>>
@GET("api/v1/instance") @GET("api/v1/instance")
fun getInstance(): Single<Instance> suspend fun getInstance(): Result<Instance>
@GET("api/v1/filters") @GET("api/v1/filters")
fun getFilters(): Single<List<Filter>> fun getFilters(): Single<List<Filter>>
@ -249,12 +249,12 @@ interface MastodonApi {
): Single<List<ScheduledStatus>> ): Single<List<ScheduledStatus>>
@DELETE("api/v1/scheduled_statuses/{id}") @DELETE("api/v1/scheduled_statuses/{id}")
fun deleteScheduledStatus( suspend fun deleteScheduledStatus(
@Path("id") scheduledStatusId: String @Path("id") scheduledStatusId: String
): Single<ResponseBody> ): Result<ResponseBody>
@GET("api/v1/accounts/verify_credentials") @GET("api/v1/accounts/verify_credentials")
fun accountVerifyCredentials(): Single<Account> suspend fun accountVerifyCredentials(): Result<Account>
@FormUrlEncoded @FormUrlEncoded
@PATCH("api/v1/accounts/update_credentials") @PATCH("api/v1/accounts/update_credentials")
@ -265,7 +265,7 @@ interface MastodonApi {
@Multipart @Multipart
@PATCH("api/v1/accounts/update_credentials") @PATCH("api/v1/accounts/update_credentials")
fun accountUpdateCredentials( suspend fun accountUpdateCredentials(
@Part(value = "display_name") displayName: RequestBody?, @Part(value = "display_name") displayName: RequestBody?,
@Part(value = "note") note: RequestBody?, @Part(value = "note") note: RequestBody?,
@Part(value = "locked") locked: RequestBody?, @Part(value = "locked") locked: RequestBody?,
@ -279,7 +279,7 @@ interface MastodonApi {
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
): Call<Account> ): Result<Account>
@GET("api/v1/accounts/search") @GET("api/v1/accounts/search")
fun searchAccounts( fun searchAccounts(
@ -447,7 +447,7 @@ interface MastodonApi {
@Field("redirect_uris") redirectUris: String, @Field("redirect_uris") redirectUris: String,
@Field("scopes") scopes: String, @Field("scopes") scopes: String,
@Field("website") website: String @Field("website") website: String
): AppCredentials ): Result<AppCredentials>
@FormUrlEncoded @FormUrlEncoded
@POST("oauth/token") @POST("oauth/token")
@ -458,7 +458,7 @@ interface MastodonApi {
@Field("redirect_uri") redirectUri: String, @Field("redirect_uri") redirectUri: String,
@Field("code") code: String, @Field("code") code: String,
@Field("grant_type") grantType: String @Field("grant_type") grantType: String
): AccessToken ): Result<AccessToken>
@FormUrlEncoded @FormUrlEncoded
@POST("api/v1/lists") @POST("api/v1/lists")

View file

@ -20,6 +20,7 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.randomAlphanumericString import com.keylesspalace.tusky.util.randomAlphanumericString
import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.launch
import io.reactivex.rxjava3.kotlin.addTo
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import retrofit2.Call import retrofit2.HttpException
import retrofit2.Callback
import retrofit2.Response
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor(
private var oldProfileData: Account? = null private var oldProfileData: Account? = null
private val disposables = CompositeDisposable() fun obtainProfile() = viewModelScope.launch {
fun obtainProfile() {
if (profileData.value == null || profileData.value is Error) { if (profileData.value == null || profileData.value is Error) {
profileData.postValue(Loading()) profileData.postValue(Loading())
mastodonApi.accountVerifyCredentials() mastodonApi.accountVerifyCredentials().fold(
.subscribe( { profile ->
{ profile -> oldProfileData = profile
oldProfileData = profile profileData.postValue(Success(profile))
profileData.postValue(Success(profile)) },
}, {
{ profileData.postValue(Error())
profileData.postValue(Error()) }
} )
)
.addTo(disposables)
} }
} }
@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor(
return return
} }
mastodonApi.accountUpdateCredentials( viewModelScope.launch {
displayName, note, locked, avatar, header, mastodonApi.accountUpdateCredentials(
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second displayName, note, locked, avatar, header,
).enqueue(object : Callback<Account> { field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
override fun onResponse(call: Call<Account>, response: Response<Account>) { ).fold(
val newProfileData = response.body() { newProfileData ->
if (!response.isSuccessful || newProfileData == null) { saveData.postValue(Success())
val errorResponse = response.errorBody()?.string() eventHub.dispatch(ProfileEditedEvent(newProfileData))
val errorMsg = if (!errorResponse.isNullOrBlank()) { },
try { { throwable ->
JSONObject(errorResponse).optString("error", null) if (throwable is HttpException) {
} catch (e: JSONException) { val errorResponse = throwable.response()?.errorBody()?.string()
val errorMsg = if (!errorResponse.isNullOrBlank()) {
try {
JSONObject(errorResponse).optString("error", "")
} catch (e: JSONException) {
null
}
} else {
null null
} }
saveData.postValue(Error(errorMessage = errorMsg))
} else { } else {
null saveData.postValue(Error())
} }
saveData.postValue(Error(errorMessage = errorMsg))
return
} }
saveData.postValue(Success()) )
eventHub.dispatch(ProfileEditedEvent(newProfileData)) }
}
override fun onFailure(call: Call<Account>, t: Throwable) {
saveData.postValue(Error())
}
})
} }
// cache activity state for rotation change // cache activity state for rotation change
@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor(
return File(application.cacheDir, filename) return File(application.cacheDir, filename)
} }
override fun onCleared() { fun obtainInstance() = viewModelScope.launch {
disposables.dispose()
}
fun obtainInstance() {
if (instanceData.value == null || instanceData.value is Error) { if (instanceData.value == null || instanceData.value is Error) {
instanceData.postValue(Loading()) instanceData.postValue(Loading())
mastodonApi.getInstance().subscribe( mastodonApi.getInstance().fold(
{ instance -> { instance ->
instanceData.postValue(Success(instance)) instanceData.postValue(Success(instance))
}, },
@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor(
instanceData.postValue(Error()) instanceData.postValue(Error())
} }
) )
.addTo(disposables)
} }
} }
} }

View file

@ -16,15 +16,11 @@
package com.keylesspalace.tusky package com.keylesspalace.tusky
import android.text.SpannedString import android.text.SpannedString
import android.widget.LinearLayout
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.SearchResult
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
@ -39,8 +35,8 @@ import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mockito.eq import org.mockito.Mockito.eq
import org.mockito.Mockito.mock import org.mockito.kotlin.doReturn
import java.util.ArrayList import org.mockito.kotlin.mock
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -306,7 +302,7 @@ class BottomSheetActivityTest {
init { init {
mastodonApi = api mastodonApi = api
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout> bottomSheet = mock()
} }
override fun openLink(url: String) { override fun openLink(url: String) {

View file

@ -24,8 +24,6 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
import com.keylesspalace.tusky.components.compose.MediaUploader
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
@ -37,18 +35,16 @@ import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.entity.InstanceConfiguration import com.keylesspalace.tusky.entity.InstanceConfiguration
import com.keylesspalace.tusky.entity.StatusConfiguration import com.keylesspalace.tusky.entity.StatusConfiguration
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient
import com.nhaarman.mockitokotlin2.any
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.core.SingleObserver
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mockito.`when` import org.mockito.kotlin.any
import org.mockito.Mockito.mock import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.Robolectric import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ -94,44 +90,47 @@ class ComposeActivityTest {
val controller = Robolectric.buildActivity(ComposeActivity::class.java) val controller = Robolectric.buildActivity(ComposeActivity::class.java)
activity = controller.get() activity = controller.get()
accountManagerMock = mock(AccountManager::class.java) accountManagerMock = mock {
`when`(accountManagerMock.activeAccount).thenReturn(account) on { activeAccount } doReturn account
}
apiMock = mock(MastodonApi::class.java) apiMock = mock {
`when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList())) on { getCustomEmojis() } doReturn Single.just(emptyList())
`when`(apiMock.getInstance()).thenReturn(object : Single<Instance>() { onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
override fun subscribeActual(observer: SingleObserver<in Instance>) {
val instance = instanceResponseCallback?.invoke()
if (instance == null) { if (instance == null) {
observer.onError(Throwable()) Result.failure(Throwable())
} else { } else {
observer.onSuccess(instance) Result.success(instance)
} }
} }
}) }
val instanceDaoMock = mock(InstanceDao::class.java) val instanceDaoMock: InstanceDao = mock {
`when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn( on { loadMetadataForInstance(any()) } doReturn
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
) on { loadMetadataForInstance(any()) } doReturn
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
}
val dbMock = mock(AppDatabase::class.java) val dbMock: AppDatabase = mock {
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock) on { instanceDao() } doReturn instanceDaoMock
}
val viewModel = ComposeViewModel( val viewModel = ComposeViewModel(
apiMock, apiMock,
accountManagerMock, accountManagerMock,
mock(MediaUploader::class.java), mock(),
mock(ServiceClient::class.java), mock(),
mock(DraftHelper::class.java), mock(),
dbMock dbMock
) )
activity.intent = Intent(activity, ComposeActivity::class.java).apply { activity.intent = Intent(activity, ComposeActivity::class.java).apply {
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions) putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
} }
val viewModelFactoryMock = mock(ViewModelFactory::class.java) val viewModelFactoryMock: ViewModelFactory = mock {
`when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel) on { create(ComposeViewModel::class.java) } doReturn viewModel
}
activity.accountManager = accountManagerMock activity.accountManager = accountManagerMock
activity.viewModelFactory = viewModelFactoryMock activity.viewModelFactory = viewModelFactoryMock
@ -490,7 +489,7 @@ class ComposeActivityTest {
) )
} }
fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration {
return InstanceConfiguration( return InstanceConfiguration(
statuses = StatusConfiguration( statuses = StatusConfiguration(
maxCharacters = maximumStatusCharacters, maxCharacters = maximumStatusCharacters,

View file

@ -8,12 +8,12 @@ import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.PollOption
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.FilterModel
import com.nhaarman.mockitokotlin2.mock
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import java.util.ArrayList import java.util.ArrayList
import java.util.Date import java.util.Date
@ -22,7 +22,7 @@ import java.util.Date
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class FilterTest { class FilterTest {
lateinit var filterModel: FilterModel private lateinit var filterModel: FilterModel
@Before @Before
fun setup() { fun setup() {

View file

@ -17,9 +17,6 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.db.TimelineStatusWithAccount
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -31,6 +28,9 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.Shadows.shadowOf import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import retrofit2.HttpException import retrofit2.HttpException

View file

@ -3,11 +3,11 @@ package com.keylesspalace.tusky.components.timeline
import androidx.paging.PagingSource import androidx.paging.PagingSource
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
class NetworkTimelinePagingSourceTest { class NetworkTimelinePagingSourceTest {

View file

@ -12,11 +12,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.doThrow
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Headers import okhttp3.Headers
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
@ -24,6 +19,11 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
@ -331,7 +331,6 @@ class NetworkTimelineRemoteMediatorTest {
mockStatusViewData("2"), mockStatusViewData("2"),
mockStatusViewData("1"), mockStatusViewData("1"),
) )
verify(timelineViewModel).nextKey = "0" verify(timelineViewModel).nextKey = "0"
assertTrue(result is RemoteMediator.MediatorResult.Success) assertTrue(result is RemoteMediator.MediatorResult.Success)
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)