refactor: Remove rxjava3 from Mastodon API spec (#128)

Remove the rxjava3 `Single` type from the MastodonAPI definition,
replacing with `Response` or `NetworkResult` as appropriate.

Update callsites and tests as appropriate.

This removes the need for `com.squareup.retrofit2:adapter-rxjava3`
This commit is contained in:
Nik Clayton 2023-09-27 11:35:55 +02:00 committed by GitHub
parent 11fecb1914
commit c5a5540467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 153 additions and 202 deletions

View File

@ -23,16 +23,15 @@ import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import app.pachli.components.account.AccountActivity
import app.pachli.components.viewthread.ViewThreadActivity
import app.pachli.network.MastodonApi
import app.pachli.util.looksLikeMastodonUrl
import app.pachli.util.openLink
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
import autodispose2.autoDispose
import at.connyduck.calladapter.networkresult.fold
import com.google.android.material.bottomsheet.BottomSheetBehavior
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import javax.inject.Inject
/** this is the base class for all activities that open links
@ -73,41 +72,34 @@ abstract class BottomSheetActivity : BaseActivity() {
return
}
mastodonApi.searchObservable(
query = url,
resolve = true,
).observeOn(AndroidSchedulers.mainThread())
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ (accounts, statuses) ->
if (getCancelSearchRequested(url)) {
return@subscribe
}
onEndSearch(url)
if (statuses.isNotEmpty()) {
viewThread(statuses[0].id, statuses[0].url)
return@subscribe
}
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let { account ->
// Some servers return (unrelated) accounts for url searches (#2804)
// Verify that the account's url matches the query
viewAccount(account.id)
return@subscribe
}
performUrlFallbackAction(url, lookupFallbackBehavior)
},
{
if (!getCancelSearchRequested(url)) {
onEndSearch(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}
},
)
onBeginSearch(url)
lifecycleScope.launch {
mastodonApi.search(query = url, resolve = true).fold({ searchResult ->
val (accounts, statuses) = searchResult
if (getCancelSearchRequested(url)) return@fold
onEndSearch(url)
statuses.firstOrNull()?.let {
viewThread(it.id, it.url)
return@fold
}
// Some servers return (unrelated) accounts for url searches (#2804)
// Verify that the account's url matches the query
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let {
viewAccount(it.id)
return@fold
}
performUrlFallbackAction(url, lookupFallbackBehavior)
}, {
if (!getCancelSearchRequested(url)) {
onEndSearch(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}
},)
}
}
open fun viewThread(statusId: String, url: String?) {
@ -138,14 +130,10 @@ abstract class BottomSheetActivity : BaseActivity() {
}
@VisibleForTesting
fun getCancelSearchRequested(url: String): Boolean {
return url != searchUrl
}
fun getCancelSearchRequested(url: String) = url != searchUrl
@VisibleForTesting
fun isSearching(): Boolean {
return searchUrl != null
}
fun isSearching() = searchUrl != null
@VisibleForTesting
fun onEndSearch(url: String?) {

View File

@ -19,7 +19,6 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ConcatAdapter
@ -55,12 +54,9 @@ import app.pachli.util.show
import app.pachli.util.viewBinding
import app.pachli.view.EndlessOnScrollListener
import at.connyduck.calladapter.networkresult.fold
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose
import com.google.android.material.color.MaterialColors
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import retrofit2.Response
import java.io.IOException
@ -258,13 +254,12 @@ class AccountListFragment :
accountId: String,
position: Int,
) {
if (accept) {
api.authorizeFollowRequest(accountId)
} else {
api.rejectFollowRequest(accountId)
}.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
viewLifecycleOwner.lifecycleScope.launch {
if (accept) {
api.authorizeFollowRequest(accountId)
} else {
api.rejectFollowRequest(accountId)
}.fold(
{
onRespondToFollowRequestSuccess(position)
},
@ -277,6 +272,7 @@ class AccountListFragment :
Log.e(TAG, "Failed to $verb account id $accountId.", throwable)
},
)
}
}
private fun onRespondToFollowRequestSuccess(position: Int) {

View File

@ -4,7 +4,6 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -20,11 +19,8 @@ import app.pachli.util.show
import app.pachli.util.viewBinding
import app.pachli.view.EndlessOnScrollListener
import at.connyduck.calladapter.networkresult.fold
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -101,23 +97,16 @@ class InstanceListFragment :
binding.recyclerView.post { adapter.bottomLoading = true }
}
api.domainBlocks(id, bottomId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ response ->
val instances = response.body()
viewLifecycleOwner.lifecycleScope.launch {
val response = api.domainBlocks(id, bottomId)
val instances = response.body()
if (response.isSuccessful && instances != null) {
onFetchInstancesSuccess(instances, response.headers()["Link"])
} else {
onFetchInstancesFailure(Exception(response.message()))
}
},
{ throwable ->
onFetchInstancesFailure(throwable)
},
)
if (response.isSuccessful && instances != null) {
onFetchInstancesSuccess(instances, response.headers()["Link"])
} else {
onFetchInstancesFailure(Exception(response.message()))
}
}
}
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
@ -158,6 +147,6 @@ class InstanceListFragment :
}
companion object {
private const val TAG = "InstanceList" // logging tag
private const val TAG = "InstanceList"
}
}

View File

@ -71,7 +71,6 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import retrofit2.HttpException
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@ -436,9 +435,9 @@ class NotificationsViewModel @Inject constructor(
try {
when (action) {
is NotificationAction.AcceptFollowRequest ->
timelineCases.acceptFollowRequest(action.accountId).await()
timelineCases.acceptFollowRequest(action.accountId)
is NotificationAction.RejectFollowRequest ->
timelineCases.rejectFollowRequest(action.accountId).await()
timelineCases.rejectFollowRequest(action.accountId)
}
uiSuccess.emit(NotificationActionSuccess.from(action))
} catch (e: Exception) {

View File

@ -43,7 +43,10 @@ class StatusesPagingSource(
withContext(Dispatchers.IO) {
val initialStatus = async { getSingleStatus(key) }
val additionalStatuses = async { getStatusList(maxId = key, limit = params.loadSize - 1) }
listOf(initialStatus.await()) + additionalStatuses.await()
buildList {
initialStatus.await()?.let { this.add(it) }
additionalStatuses.await()?.let { this.addAll(it) }
}
}
} else {
val maxId = if (params is LoadParams.Refresh || params is LoadParams.Append) {
@ -58,7 +61,7 @@ class StatusesPagingSource(
null
}
getStatusList(minId = minId, maxId = maxId, limit = params.loadSize)
getStatusList(minId = minId, maxId = maxId, limit = params.loadSize) ?: emptyList()
}
return LoadResult.Page(
data = result,
@ -71,18 +74,18 @@ class StatusesPagingSource(
}
}
private suspend fun getSingleStatus(statusId: String): Status {
return mastodonApi.statusObservable(statusId).await()
private suspend fun getSingleStatus(statusId: String): Status? {
return mastodonApi.status(statusId).getOrNull()
}
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status> {
return mastodonApi.accountStatusesObservable(
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status>? {
return mastodonApi.accountStatuses(
accountId = accountId,
maxId = maxId,
sinceId = null,
minId = minId,
limit = limit,
excludeReblogs = true,
).await()
).body()
}
}

View File

@ -15,12 +15,11 @@
package app.pachli.components.scheduled
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import app.pachli.entity.ScheduledStatus
import app.pachli.network.MastodonApi
import kotlinx.coroutines.rx3.await
import at.connyduck.calladapter.networkresult.getOrElse
class ScheduledStatusPagingSourceFactory(
private val mastodonApi: MastodonApi,
@ -59,21 +58,12 @@ class ScheduledStatusPagingSource(
nextKey = scheduledStatusesCache.lastOrNull()?.id,
)
} else {
try {
val result = mastodonApi.scheduledStatuses(
maxId = params.key,
limit = params.loadSize,
).await()
val result = mastodonApi.scheduledStatuses(
maxId = params.key,
limit = params.loadSize,
).getOrElse { return LoadResult.Error(it) }
LoadResult.Page(
data = result,
prevKey = null,
nextKey = result.lastOrNull()?.id,
)
} catch (e: Exception) {
Log.w("ScheduledStatuses", "Error loading scheduled statuses", e)
LoadResult.Error(e)
}
LoadResult.Page(data = result, prevKey = null, nextKey = result.lastOrNull()?.id)
}
}
}

View File

@ -20,7 +20,7 @@ import androidx.paging.PagingState
import app.pachli.components.search.SearchType
import app.pachli.entity.SearchResult
import app.pachli.network.MastodonApi
import kotlinx.coroutines.rx3.await
import at.connyduck.calladapter.networkresult.getOrElse
class SearchPagingSource<T : Any>(
private val mastodonApi: MastodonApi,
@ -53,31 +53,27 @@ class SearchPagingSource<T : Any>(
val currentKey = params.key ?: 0
try {
val data = mastodonApi.searchObservable(
query = searchRequest,
type = searchType.apiParameter,
resolve = true,
limit = params.loadSize,
offset = currentKey,
following = false,
).await()
val data = mastodonApi.search(
query = searchRequest,
type = searchType.apiParameter,
resolve = true,
limit = params.loadSize,
offset = currentKey,
following = false,
).getOrElse { return LoadResult.Error(it) }
val res = parser(data)
val res = parser(data)
val nextKey = if (res.isEmpty()) {
null
} else {
currentKey + res.size
}
return LoadResult.Page(
data = res,
prevKey = null,
nextKey = nextKey,
)
} catch (e: Exception) {
return LoadResult.Error(e)
val nextKey = if (res.isEmpty()) {
null
} else {
currentKey + res.size
}
return LoadResult.Page(
data = res,
prevKey = null,
nextKey = nextKey,
)
}
}

View File

@ -40,7 +40,6 @@ import okhttp3.OkHttp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.net.IDN
@ -115,7 +114,6 @@ class NetworkModule {
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
.build()
}

View File

@ -47,7 +47,6 @@ import app.pachli.entity.TrendingTag
import app.pachli.entity.TrendsLink
import app.pachli.util.HttpHeaderLink
import at.connyduck.calladapter.networkresult.NetworkResult
import io.reactivex.rxjava3.core.Single
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.ResponseBody
@ -312,10 +311,10 @@ interface MastodonApi {
): NetworkResult<Status>
@GET("api/v1/scheduled_statuses")
fun scheduledStatuses(
suspend fun scheduledStatuses(
@Query("limit") limit: Int? = null,
@Query("max_id") maxId: String? = null,
): Single<List<ScheduledStatus>>
): NetworkResult<List<ScheduledStatus>>
@DELETE("api/v1/scheduled_statuses/{id}")
suspend fun deleteScheduledStatus(
@ -378,9 +377,11 @@ interface MastodonApi {
/**
* Method to fetch statuses for the specified account.
* @param accountId ID for account for which statuses will be requested
* @param maxId Only statuses with ID less than maxID will be returned
* @param sinceId Only statuses with ID bigger than sinceID will be returned
* @param limit Limit returned statuses (current API limits: default - 20, max - 40)
* @param maxId Only statuses with ID less than maxId will be returned
* @param sinceId Only statuses with ID bigger than sinceId will be returned
* @param minId Only statuses with ID greater than minId will be returned
* @param limit Limit returned statuses
* @param excludeReblogs only return statuses that are not reblogs
* @param excludeReplies only return statuses that are no replies
* @param onlyMedia only return statuses that have media attached
*/
@ -391,6 +392,7 @@ interface MastodonApi {
@Query("since_id") sinceId: String? = null,
@Query("min_id") minId: String? = null,
@Query("limit") limit: Int? = null,
@Query("exclude_reblogs") excludeReblogs: Boolean? = null,
@Query("exclude_replies") excludeReplies: Boolean? = null,
@Query("only_media") onlyMedia: Boolean? = null,
@Query("pinned") pinned: Boolean? = null,
@ -470,11 +472,11 @@ interface MastodonApi {
): Response<List<TimelineAccount>>
@GET("api/v1/domain_blocks")
fun domainBlocks(
suspend fun domainBlocks(
@Query("max_id") maxId: String? = null,
@Query("since_id") sinceId: String? = null,
@Query("limit") limit: Int? = null,
): Single<Response<List<String>>>
): Response<List<String>>
@FormUrlEncoded
@POST("api/v1/domain_blocks")
@ -509,14 +511,14 @@ interface MastodonApi {
): Response<List<TimelineAccount>>
@POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequest(
suspend fun authorizeFollowRequest(
@Path("id") accountId: String,
): Single<Relationship>
): NetworkResult<Relationship>
@POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequest(
suspend fun rejectFollowRequest(
@Path("id") accountId: String,
): Single<Relationship>
): NetworkResult<Relationship>
@FormUrlEncoded
@POST("api/v1/apps")
@ -716,34 +718,19 @@ interface MastodonApi {
@Field("forward") isNotifyRemote: Boolean?,
): NetworkResult<Unit>
@GET("api/v1/accounts/{id}/statuses")
fun accountStatusesObservable(
@Path("id") accountId: String,
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("min_id") minId: String?,
@Query("limit") limit: Int?,
@Query("exclude_reblogs") excludeReblogs: Boolean?,
): Single<List<Status>>
@GET("api/v1/statuses/{id}")
fun statusObservable(
@Path("id") statusId: String,
): Single<Status>
@GET("api/v2/search")
fun searchObservable(
suspend fun search(
@Query("q") query: String?,
@Query("type") type: String? = null,
@Query("resolve") resolve: Boolean? = null,
@Query("limit") limit: Int? = null,
@Query("offset") offset: Int? = null,
@Query("following") following: Boolean? = null,
): Single<SearchResult>
): NetworkResult<SearchResult>
@GET("api/v2/search")
fun searchSync(
@Query("q") query: String?,
@Query("q") query: String,
@Query("type") type: String? = null,
@Query("resolve") resolve: Boolean? = null,
@Query("limit") limit: Int? = null,

View File

@ -36,7 +36,6 @@ import at.connyduck.calladapter.networkresult.NetworkResult
import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure
import at.connyduck.calladapter.networkresult.onSuccess
import io.reactivex.rxjava3.core.Single
import javax.inject.Inject
class TimelineCases @Inject constructor(
@ -132,11 +131,11 @@ class TimelineCases @Inject constructor(
}
}
fun acceptFollowRequest(accountId: String): Single<Relationship> {
suspend fun acceptFollowRequest(accountId: String): NetworkResult<Relationship> {
return mastodonApi.authorizeFollowRequest(accountId)
}
fun rejectFollowRequest(accountId: String): Single<Relationship> {
suspend fun rejectFollowRequest(accountId: String): NetworkResult<Relationship> {
return mastodonApi.rejectFollowRequest(accountId)
}

View File

@ -18,14 +18,16 @@
package app.pachli
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.pachli.components.timeline.MainCoroutineRule
import app.pachli.entity.SearchResult
import app.pachli.entity.Status
import app.pachli.entity.TimelineAccount
import app.pachli.network.MastodonApi
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.TestScheduler
import at.connyduck.calladapter.networkresult.NetworkResult
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@ -37,21 +39,25 @@ import org.mockito.Mockito.eq
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import java.util.Date
import java.util.concurrent.TimeUnit
@OptIn(ExperimentalCoroutinesApi::class)
class BottomSheetActivityTest {
@get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainCoroutineRule = MainCoroutineRule(dispatcher = StandardTestDispatcher())
private lateinit var activity: FakeBottomSheetActivity
private lateinit var apiMock: MastodonApi
private val accountQuery = "http://mastodon.foo.bar/@User"
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
private val nonexistentStatusQuery = "http://mastodon.foo.bar/@User/345678000"
private val nonMastodonQuery = "http://medium.com/@correspondent/345678"
private val emptyCallback = Single.just(SearchResult(emptyList(), emptyList(), emptyList()))
private val testScheduler = TestScheduler()
private val emptyResponse = NetworkResult.success(
SearchResult(emptyList(), emptyList(), emptyList()),
)
private val account = TimelineAccount(
id = "1",
@ -62,7 +68,9 @@ class BottomSheetActivityTest {
url = "http://mastodon.foo.bar/@User",
avatar = "",
)
private val accountSingle = Single.just(SearchResult(listOf(account), emptyList(), emptyList()))
private val accountResponse = NetworkResult.success(
SearchResult(listOf(account), emptyList(), emptyList()),
)
private val status = Status(
id = "1",
@ -95,31 +103,30 @@ class BottomSheetActivityTest {
language = null,
filtered = null,
)
private val statusSingle = Single.just(SearchResult(emptyList(), listOf(status), emptyList()))
private val statusResponse = NetworkResult.success(
SearchResult(emptyList(), listOf(status), emptyList()),
)
@Before
fun setup() {
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
RxAndroidPlugins.setMainThreadSchedulerHandler { testScheduler }
apiMock = mock {
on { searchObservable(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountSingle
on { searchObservable(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusSingle
on { searchObservable(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountSingle
on { searchObservable(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyCallback
onBlocking { search(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResponse
onBlocking { search(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusResponse
onBlocking { search(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResponse
onBlocking { search(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyResponse
}
activity = FakeBottomSheetActivity(apiMock)
}
@Test
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() = runTest {
activity.onBeginSearch("https://mastodon.foo.bar/@User")
assertTrue(activity.isSearching())
}
@Test
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() {
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() = runTest {
val validUrl = "https://mastodon.foo.bar/@User"
activity.onBeginSearch(validUrl)
activity.onEndSearch(validUrl)
@ -127,7 +134,7 @@ class BottomSheetActivityTest {
}
@Test
fun beginEndSearch_setIsSearching_doesNotCancelSearchWhenResponseFromPreviousSearchIsReceived() {
fun beginEndSearch_setIsSearching_doesNotCancelSearchWhenResponseFromPreviousSearchIsReceived() = runTest {
val validUrl = "https://mastodon.foo.bar/@User"
val invalidUrl = ""
@ -137,7 +144,7 @@ class BottomSheetActivityTest {
}
@Test
fun cancelActiveSearch() {
fun cancelActiveSearch() = runTest {
val url = "https://mastodon.foo.bar/@User"
activity.onBeginSearch(url)
@ -146,7 +153,7 @@ class BottomSheetActivityTest {
}
@Test
fun getCancelSearchRequested_detectsURL() {
fun getCancelSearchRequested_detectsURL() = runTest {
val firstUrl = "https://mastodon.foo.bar/@User"
val secondUrl = "https://mastodon.foo.bar/@meh"
@ -159,46 +166,46 @@ class BottomSheetActivityTest {
}
@Test
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
fun search_inIdealConditions_returnsRequestedResults_forAccount() = runTest {
activity.viewUrl(accountQuery)
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
advanceUntilIdle()
assertEquals(account.id, activity.accountId)
}
@Test
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
fun search_inIdealConditions_returnsRequestedResults_forStatus() = runTest {
activity.viewUrl(statusQuery)
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
advanceUntilIdle()
assertEquals(status.id, activity.statusId)
}
@Test
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() = runTest {
activity.viewUrl(nonMastodonQuery)
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
advanceUntilIdle()
assertEquals(nonMastodonQuery, activity.link)
}
@Test
fun search_withNoResults_appliesRequestedFallbackBehavior() {
fun search_withNoResults_appliesRequestedFallbackBehavior() = runTest {
for (fallbackBehavior in listOf(PostLookupFallbackBehavior.OPEN_IN_BROWSER, PostLookupFallbackBehavior.DISPLAY_ERROR)) {
activity.viewUrl(nonMastodonQuery, fallbackBehavior)
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
advanceUntilIdle()
assertEquals(nonMastodonQuery, activity.link)
assertEquals(fallbackBehavior, activity.fallbackBehavior)
}
}
@Test
fun search_doesNotRespectUnrelatedResult() {
fun search_doesNotRespectUnrelatedResult() = runTest {
activity.viewUrl(nonexistentStatusQuery)
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
advanceUntilIdle()
assertEquals(nonexistentStatusQuery, activity.link)
assertEquals(null, activity.accountId)
}
@Test
fun search_withCancellation_doesNotLoadUrl_forAccount() {
fun search_withCancellation_doesNotLoadUrl_forAccount() = runTest {
activity.viewUrl(accountQuery)
assertTrue(activity.isSearching())
activity.cancelActiveSearch()
@ -207,21 +214,21 @@ class BottomSheetActivityTest {
}
@Test
fun search_withCancellation_doesNotLoadUrl_forStatus() {
fun search_withCancellation_doesNotLoadUrl_forStatus() = runTest {
activity.viewUrl(accountQuery)
activity.cancelActiveSearch()
assertEquals(null, activity.accountId)
}
@Test
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() = runTest {
activity.viewUrl(nonMastodonQuery)
activity.cancelActiveSearch()
assertEquals(null, activity.searchUrl)
}
@Test
fun search_withPreviousCancellation_completes() {
fun search_withPreviousCancellation_completes() = runTest {
// begin/cancel account search
activity.viewUrl(accountQuery)
activity.cancelActiveSearch()
@ -233,7 +240,7 @@ class BottomSheetActivityTest {
assertTrue(activity.isSearching())
// return searchResults
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
advanceUntilIdle()
// ensure that the result of the status search was recorded
// and the account search wasn't

View File

@ -19,8 +19,8 @@ package app.pachli.components.notifications
import app.cash.turbine.test
import app.pachli.entity.Relationship
import at.connyduck.calladapter.networkresult.NetworkResult
import com.google.common.truth.Truth.assertThat
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.mockito.kotlin.any
@ -66,7 +66,7 @@ class NotificationsViewModelTestNotificationAction : NotificationsViewModelTestB
fun `accepting follow request succeeds && emits UiSuccess`() = runTest {
// Given
timelineCases.stub {
onBlocking { acceptFollowRequest(any()) } doReturn Single.just(relationship)
onBlocking { acceptFollowRequest(any()) } doReturn NetworkResult.success(relationship)
}
viewModel.uiSuccess.test {
@ -105,7 +105,7 @@ class NotificationsViewModelTestNotificationAction : NotificationsViewModelTestB
@Test
fun `rejecting follow request succeeds && emits UiSuccess`() = runTest {
// Given
timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn Single.just(relationship) }
timelineCases.stub { onBlocking { rejectFollowRequest(any()) } doReturn NetworkResult.success(relationship) }
viewModel.uiSuccess.test {
// When

View File

@ -137,7 +137,6 @@ mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "
networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" }
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
retrofit-adapter-rxjava3 = { module = "com.squareup.retrofit2:adapter-rxjava3", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
@ -169,7 +168,7 @@ glide = ["glide-core", "glide-okhttp3-integration", "glide-animation-plugin"]
material-drawer = ["material-drawer-core", "material-drawer-iconics"]
mockito = ["mockito-kotlin", "mockito-inline"]
okhttp = ["okhttp-core", "okhttp-logging-interceptor"]
retrofit = ["retrofit-core", "retrofit-converter-gson", "retrofit-adapter-rxjava3"]
retrofit = ["retrofit-core", "retrofit-converter-gson"]
room = ["androidx-room-ktx", "androidx-room-paging"]
rxjava3 = ["rxjava3-core", "rxjava3-android", "rxjava3-kotlin"]
xmldiff = ["diffx", "xmlwriter"]