introduce KotlinResultCallAdapter for nice suspending network calls (#2415)
* introduce KotlinResultCallAdapter for nice suspending network calls * fix tests
This commit is contained in:
parent
d21d045eda
commit
3e8c6a318a
15 changed files with 168 additions and 179 deletions
|
@ -137,6 +137,7 @@ dependencies {
|
|||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$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:logging-interceptor:$okhttpVersion"
|
||||
|
@ -176,8 +177,8 @@ dependencies {
|
|||
|
||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
testImplementation "org.mockito:mockito-inline:3.6.28"
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation "org.mockito:mockito-inline:4.4.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||
|
|
|
@ -682,18 +682,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
}
|
||||
|
||||
private fun fetchUserInfo() {
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
private fun fetchUserInfo() = lifecycleScope.launch {
|
||||
mastodonApi.accountVerifyCredentials().fold(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onFetchUserInfoSuccess(me: Account) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource
|
|||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
|
@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
rxSingle {
|
||||
mastodonApi.getInstance().getOrThrow()
|
||||
}.map { Either.Right(it) }
|
||||
}
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
|
|
|
@ -48,6 +48,7 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -105,7 +106,10 @@ class ComposeViewModel @Inject constructor(
|
|||
init {
|
||||
|
||||
Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance()
|
||||
api.getCustomEmojis(),
|
||||
rxSingle {
|
||||
api.getInstance().getOrThrow()
|
||||
}
|
||||
) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
|
@ -291,7 +295,7 @@ class ComposeViewModel @Inject constructor(
|
|||
): LiveData<Unit> {
|
||||
|
||||
val deletionObservable = if (isEditingScheduledToot) {
|
||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||
rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
|
||||
} else {
|
||||
Observable.just(Unit)
|
||||
}.toLiveData()
|
||||
|
|
|
@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.AppCredentials
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
import com.keylesspalace.tusky.util.rickRoll
|
||||
|
@ -166,32 +165,33 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
setLoading(true)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val credentials: AppCredentials = try {
|
||||
mastodonApi.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_failed_app_registration)
|
||||
setLoading(false)
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@launch
|
||||
}
|
||||
mastodonApi.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
).fold(
|
||||
{ credentials ->
|
||||
// Before we open browser page we save the data.
|
||||
// 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.
|
||||
// Alternatively we could pass it all as part of the intent and receive it back
|
||||
// but it is a bit of a workaround.
|
||||
preferences.edit()
|
||||
.putString(DOMAIN, domain)
|
||||
.putString(CLIENT_ID, credentials.clientId)
|
||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||
.apply()
|
||||
|
||||
// Before we open browser page we save the data.
|
||||
// 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.
|
||||
// Alternatively we could pass it all as part of the intent and receive it back
|
||||
// but it is a bit of a workaround.
|
||||
preferences.edit()
|
||||
.putString(DOMAIN, domain)
|
||||
.putString(CLIENT_ID, credentials.clientId)
|
||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||
.apply()
|
||||
|
||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||
},
|
||||
{ e ->
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error =
|
||||
getString(R.string.error_failed_app_registration)
|
||||
setLoading(false)
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@launch
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,29 +224,28 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
setLoading(true)
|
||||
|
||||
val accessToken = try {
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, oauthRedirectUri, code,
|
||||
"authorization_code"
|
||||
)
|
||||
} 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
|
||||
}
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||
).fold(
|
||||
{ accessToken ->
|
||||
accountManager.addAccount(accessToken.accessToken, domain)
|
||||
|
||||
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
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
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) {
|
||||
|
|
|
@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScheduledStatusViewModel @Inject constructor(
|
||||
|
@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor(
|
|||
|
||||
fun deleteScheduledStatus(status: ScheduledStatus) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
mastodonApi.deleteScheduledStatus(status.id).await()
|
||||
pagingSourceFactory.remove(status)
|
||||
} catch (throwable: Throwable) {
|
||||
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
||||
}
|
||||
mastodonApi.deleteScheduledStatus(status.id).fold(
|
||||
{
|
||||
pagingSourceFactory.remove(status)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.text.Spanned
|
||||
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
|
@ -111,6 +112,7 @@ class NetworkModule {
|
|||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ interface MastodonApi {
|
|||
fun getCustomEmojis(): Single<List<Emoji>>
|
||||
|
||||
@GET("api/v1/instance")
|
||||
fun getInstance(): Single<Instance>
|
||||
suspend fun getInstance(): Result<Instance>
|
||||
|
||||
@GET("api/v1/filters")
|
||||
fun getFilters(): Single<List<Filter>>
|
||||
|
@ -249,12 +249,12 @@ interface MastodonApi {
|
|||
): Single<List<ScheduledStatus>>
|
||||
|
||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||
fun deleteScheduledStatus(
|
||||
suspend fun deleteScheduledStatus(
|
||||
@Path("id") scheduledStatusId: String
|
||||
): Single<ResponseBody>
|
||||
): Result<ResponseBody>
|
||||
|
||||
@GET("api/v1/accounts/verify_credentials")
|
||||
fun accountVerifyCredentials(): Single<Account>
|
||||
suspend fun accountVerifyCredentials(): Result<Account>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
|
@ -265,7 +265,7 @@ interface MastodonApi {
|
|||
|
||||
@Multipart
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
fun accountUpdateCredentials(
|
||||
suspend fun accountUpdateCredentials(
|
||||
@Part(value = "display_name") displayName: RequestBody?,
|
||||
@Part(value = "note") note: RequestBody?,
|
||||
@Part(value = "locked") locked: RequestBody?,
|
||||
|
@ -279,7 +279,7 @@ interface MastodonApi {
|
|||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||
): Call<Account>
|
||||
): Result<Account>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccounts(
|
||||
|
@ -447,7 +447,7 @@ interface MastodonApi {
|
|||
@Field("redirect_uris") redirectUris: String,
|
||||
@Field("scopes") scopes: String,
|
||||
@Field("website") website: String
|
||||
): AppCredentials
|
||||
): Result<AppCredentials>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
|
@ -458,7 +458,7 @@ interface MastodonApi {
|
|||
@Field("redirect_uri") redirectUri: String,
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grantType: String
|
||||
): AccessToken
|
||||
): Result<AccessToken>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
|||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
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.Success
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
|
@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
|
|||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor(
|
|||
|
||||
private var oldProfileData: Account? = null
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
fun obtainProfile() {
|
||||
fun obtainProfile() = viewModelScope.launch {
|
||||
if (profileData.value == null || profileData.value is Error) {
|
||||
|
||||
profileData.postValue(Loading())
|
||||
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.subscribe(
|
||||
{ profile ->
|
||||
oldProfileData = profile
|
||||
profileData.postValue(Success(profile))
|
||||
},
|
||||
{
|
||||
profileData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposables)
|
||||
mastodonApi.accountVerifyCredentials().fold(
|
||||
{ profile ->
|
||||
oldProfileData = profile
|
||||
profileData.postValue(Success(profile))
|
||||
},
|
||||
{
|
||||
profileData.postValue(Error())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
mastodonApi.accountUpdateCredentials(
|
||||
displayName, note, locked, avatar, header,
|
||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||
).enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
val newProfileData = response.body()
|
||||
if (!response.isSuccessful || newProfileData == null) {
|
||||
val errorResponse = response.errorBody()?.string()
|
||||
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
||||
try {
|
||||
JSONObject(errorResponse).optString("error", null)
|
||||
} catch (e: JSONException) {
|
||||
viewModelScope.launch {
|
||||
mastodonApi.accountUpdateCredentials(
|
||||
displayName, note, locked, avatar, header,
|
||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||
).fold(
|
||||
{ newProfileData ->
|
||||
saveData.postValue(Success())
|
||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||
},
|
||||
{ throwable ->
|
||||
if (throwable is HttpException) {
|
||||
val errorResponse = throwable.response()?.errorBody()?.string()
|
||||
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
||||
try {
|
||||
JSONObject(errorResponse).optString("error", "")
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
saveData.postValue(Error(errorMessage = errorMsg))
|
||||
} 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
|
||||
|
@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor(
|
|||
return File(application.cacheDir, filename)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun obtainInstance() {
|
||||
fun obtainInstance() = viewModelScope.launch {
|
||||
if (instanceData.value == null || instanceData.value is Error) {
|
||||
instanceData.postValue(Loading())
|
||||
|
||||
mastodonApi.getInstance().subscribe(
|
||||
mastodonApi.getInstance().fold(
|
||||
{ instance ->
|
||||
instanceData.postValue(Success(instance))
|
||||
},
|
||||
|
@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor(
|
|||
instanceData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.addTo(disposables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,15 +16,11 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.text.SpannedString
|
||||
import android.widget.LinearLayout
|
||||
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.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
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.core.Single
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
|
@ -39,8 +35,8 @@ import org.junit.runner.RunWith
|
|||
import org.junit.runners.Parameterized
|
||||
import org.mockito.ArgumentMatchers.anyBoolean
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.mock
|
||||
import java.util.ArrayList
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -306,7 +302,7 @@ class BottomSheetActivityTest {
|
|||
init {
|
||||
mastodonApi = api
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout>
|
||||
bottomSheet = mock()
|
||||
}
|
||||
|
||||
override fun openLink(url: String) {
|
||||
|
|
|
@ -24,8 +24,6 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
|
|||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
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.MediaUploader
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
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.StatusConfiguration
|
||||
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.SingleObserver
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
@ -94,44 +90,47 @@ class ComposeActivityTest {
|
|||
val controller = Robolectric.buildActivity(ComposeActivity::class.java)
|
||||
activity = controller.get()
|
||||
|
||||
accountManagerMock = mock(AccountManager::class.java)
|
||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
||||
accountManagerMock = mock {
|
||||
on { activeAccount } doReturn account
|
||||
}
|
||||
|
||||
apiMock = mock(MastodonApi::class.java)
|
||||
`when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList()))
|
||||
`when`(apiMock.getInstance()).thenReturn(object : Single<Instance>() {
|
||||
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
||||
val instance = instanceResponseCallback?.invoke()
|
||||
apiMock = mock {
|
||||
on { getCustomEmojis() } doReturn Single.just(emptyList())
|
||||
onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance ->
|
||||
if (instance == null) {
|
||||
observer.onError(Throwable())
|
||||
Result.failure(Throwable())
|
||||
} else {
|
||||
observer.onSuccess(instance)
|
||||
Result.success(instance)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val instanceDaoMock = mock(InstanceDao::class.java)
|
||||
`when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn(
|
||||
Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null))
|
||||
)
|
||||
val instanceDaoMock: InstanceDao = mock {
|
||||
on { loadMetadataForInstance(any()) } doReturn
|
||||
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)
|
||||
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
|
||||
val dbMock: AppDatabase = mock {
|
||||
on { instanceDao() } doReturn instanceDaoMock
|
||||
}
|
||||
|
||||
val viewModel = ComposeViewModel(
|
||||
apiMock,
|
||||
accountManagerMock,
|
||||
mock(MediaUploader::class.java),
|
||||
mock(ServiceClient::class.java),
|
||||
mock(DraftHelper::class.java),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
dbMock
|
||||
)
|
||||
activity.intent = Intent(activity, ComposeActivity::class.java).apply {
|
||||
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
|
||||
}
|
||||
|
||||
val viewModelFactoryMock = mock(ViewModelFactory::class.java)
|
||||
`when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel)
|
||||
val viewModelFactoryMock: ViewModelFactory = mock {
|
||||
on { create(ComposeViewModel::class.java) } doReturn viewModel
|
||||
}
|
||||
|
||||
activity.accountManager = accountManagerMock
|
||||
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(
|
||||
statuses = StatusConfiguration(
|
||||
maxCharacters = maximumStatusCharacters,
|
||||
|
|
|
@ -8,12 +8,12 @@ import com.keylesspalace.tusky.entity.Poll
|
|||
import com.keylesspalace.tusky.entity.PollOption
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.ArrayList
|
||||
import java.util.Date
|
||||
|
@ -22,7 +22,7 @@ import java.util.Date
|
|||
@RunWith(AndroidJUnit4::class)
|
||||
class FilterTest {
|
||||
|
||||
lateinit var filterModel: FilterModel
|
||||
private lateinit var filterModel: FilterModel
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
|
|
|
@ -17,9 +17,6 @@ import com.keylesspalace.tusky.db.AccountManager
|
|||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -31,6 +28,9 @@ import org.junit.Assert.assertTrue
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
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.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
|
|
|
@ -3,11 +3,11 @@ package com.keylesspalace.tusky.components.timeline
|
|||
import androidx.paging.PagingSource
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
class NetworkTimelinePagingSourceTest {
|
||||
|
||||
|
|
|
@ -12,11 +12,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView
|
|||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
|
@ -24,6 +19,11 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
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 retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
@ -331,7 +331,6 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1"),
|
||||
)
|
||||
|
||||
verify(timelineViewModel).nextKey = "0"
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
|
Loading…
Reference in a new issue