Merge tag 'v28.0' into develop
# Conflicts: # README.md # app/build.gradle # app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt # app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java # app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt # app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt # app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt # app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.kt # app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt # app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt # app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt # app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt # app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt # app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt # app/src/main/res/color/compound_button_color.xml # app/src/main/res/color/text_input_layout_box_stroke_color.xml # app/src/main/res/drawable/ic_check_circle.xml # app/src/main/res/drawable/ic_flag_24dp.xml # app/src/main/res/drawable/ic_person_add_24dp.xml # app/src/main/res/drawable/ic_play_indicator.xml # app/src/main/res/drawable/ic_poll_24dp.xml # app/src/main/res/drawable/ic_reblog_active_24dp.xml # app/src/main/res/drawable/ic_reblog_private_active_24dp.xml # app/src/main/res/drawable/report_success_background.xml # app/src/main/res/layout-land/item_trending_cell.xml # app/src/main/res/layout/activity_account.xml # app/src/main/res/layout/activity_edit_filter.xml # app/src/main/res/layout/card_license.xml # app/src/main/res/layout/item_announcement.xml # app/src/main/res/layout/item_status.xml # app/src/main/res/layout/item_status_detailed.xml # app/src/main/res/layout/item_tab_preference.xml # app/src/main/res/layout/item_trending_cell.xml # app/src/main/res/values-cs/strings.xml # app/src/main/res/values-de/strings.xml # app/src/main/res/values-es/strings.xml # app/src/main/res/values-eu/strings.xml # app/src/main/res/values-fr/strings.xml # app/src/main/res/values-kab/strings.xml # app/src/main/res/values-lv/strings.xml # app/src/main/res/values-nb-rNO/strings.xml # app/src/main/res/values-night/theme_colors.xml # app/src/main/res/values/colors.xml # app/src/main/res/values/strings.xml # app/src/main/res/values/styles.xml # app/src/main/res/values/theme_colors.xml
This commit is contained in:
parent
320900ce4e
commit
a66f7bb515
614 changed files with 52429 additions and 19916 deletions
|
|
@ -18,7 +18,8 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.components.filters.EditFilterActivity
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.FilterV1
|
||||
|
|
@ -26,16 +27,22 @@ 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.keylesspalace.tusky.network.MastodonApi
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FilterV1Test {
|
||||
|
||||
|
|
@ -43,12 +50,11 @@ class FilterV1Test {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
filterModel = FilterModel()
|
||||
val filters = listOf(
|
||||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "badWord",
|
||||
context = listOf(FilterV1.HOME),
|
||||
context = listOf(Filter.Kind.HOME.kind),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = false
|
||||
|
|
@ -56,7 +62,7 @@ class FilterV1Test {
|
|||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "badWholeWord",
|
||||
context = listOf(FilterV1.HOME, FilterV1.PUBLIC),
|
||||
context = listOf(Filter.Kind.HOME.kind, Filter.Kind.PUBLIC.kind),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = true
|
||||
|
|
@ -64,7 +70,7 @@ class FilterV1Test {
|
|||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "@twitter.com",
|
||||
context = listOf(FilterV1.HOME),
|
||||
context = listOf(Filter.Kind.HOME.kind),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = true
|
||||
|
|
@ -72,7 +78,7 @@ class FilterV1Test {
|
|||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "#hashtag",
|
||||
context = listOf(FilterV1.HOME),
|
||||
context = listOf(Filter.Kind.HOME.kind),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = true
|
||||
|
|
@ -80,7 +86,7 @@ class FilterV1Test {
|
|||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "expired",
|
||||
context = listOf(FilterV1.HOME),
|
||||
context = listOf(Filter.Kind.HOME.kind),
|
||||
expiresAt = Date.from(Instant.now().minusSeconds(10)),
|
||||
irreversible = false,
|
||||
wholeWord = true
|
||||
|
|
@ -88,7 +94,7 @@ class FilterV1Test {
|
|||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "unexpired",
|
||||
context = listOf(FilterV1.HOME),
|
||||
context = listOf(Filter.Kind.HOME.kind),
|
||||
expiresAt = Date.from(Instant.now().plusSeconds(3600)),
|
||||
irreversible = false,
|
||||
wholeWord = true
|
||||
|
|
@ -96,14 +102,27 @@ class FilterV1Test {
|
|||
FilterV1(
|
||||
id = "123",
|
||||
phrase = "href",
|
||||
context = listOf(FilterV1.HOME),
|
||||
context = listOf(Filter.Kind.HOME.kind),
|
||||
expiresAt = null,
|
||||
irreversible = false,
|
||||
wholeWord = false
|
||||
)
|
||||
)
|
||||
|
||||
filterModel.initWithFilters(filters)
|
||||
val api: MastodonApi = mock {
|
||||
onBlocking { getFiltersV1() } doReturn NetworkResult.success(filters)
|
||||
onBlocking { getFilters() } doReturn NetworkResult.failure(
|
||||
HttpException(Response.error<Any>(404, "".toResponseBody()))
|
||||
)
|
||||
}
|
||||
val instanceInfoRepo: InstanceInfoRepository = mock {
|
||||
onBlocking { isFilterV2Supported() } doReturn false
|
||||
}
|
||||
|
||||
filterModel = FilterModel(instanceInfoRepo, api)
|
||||
runBlocking {
|
||||
filterModel.init(Filter.Kind.HOME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -257,22 +276,6 @@ class FilterV1Test {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() {
|
||||
val expiredBySeconds = 3600
|
||||
val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong()))
|
||||
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
|
||||
assert(updatedDuration != null && updatedDuration <= -expiredBySeconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() {
|
||||
val expiresInSeconds = 3600
|
||||
val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong()))
|
||||
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
|
||||
assert(updatedDuration != null && updatedDuration > (expiresInSeconds - 60))
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun mockStatus(
|
||||
content: String = "",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ package com.keylesspalace.tusky
|
|||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
|
|
@ -11,13 +14,16 @@ import androidx.work.testing.WorkManagerTestInitHelper
|
|||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.getSerializableExtraCompat
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
|
|
@ -68,7 +74,7 @@ class MainActivityTest {
|
|||
|
||||
@Test
|
||||
fun `clicking notification of type FOLLOW shows notification tab`() {
|
||||
val intent = showNotification(Notification.Type.FOLLOW)
|
||||
val intent = showNotification(Notification.Type.Follow)
|
||||
|
||||
val activity = startMainActivity(intent)
|
||||
val currentTab = activity.findViewById<ViewPager2>(R.id.viewPager).currentItem
|
||||
|
|
@ -80,26 +86,35 @@ class MainActivityTest {
|
|||
|
||||
@Test
|
||||
fun `clicking notification of type FOLLOW_REQUEST shows follow requests`() {
|
||||
val intent = showNotification(Notification.Type.FOLLOW_REQUEST)
|
||||
val intent = showNotification(Notification.Type.FollowRequest)
|
||||
|
||||
val activity = startMainActivity(intent)
|
||||
val nextActivity = shadowOf(activity).peekNextStartedActivity()
|
||||
|
||||
assertNotNull(nextActivity)
|
||||
assertEquals(ComponentName(context, AccountListActivity::class.java.name), nextActivity.component)
|
||||
assertEquals(AccountListActivity.Type.FOLLOW_REQUESTS, nextActivity.getSerializableExtra("type"))
|
||||
assertEquals(AccountListActivity.Type.FOLLOW_REQUESTS, nextActivity.getSerializableExtraCompat("type"))
|
||||
}
|
||||
|
||||
private fun showNotification(type: Notification.Type): Intent {
|
||||
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
val shadowNotificationManager = shadowOf(notificationManager)
|
||||
|
||||
NotificationHelper.createNotificationChannelsForAccount(accountEntity, context)
|
||||
val notificationService = NotificationService(
|
||||
notificationManager,
|
||||
mock {
|
||||
on { areNotificationsEnabled() } doReturn true
|
||||
},
|
||||
mock(),
|
||||
mock(),
|
||||
context,
|
||||
mock(),
|
||||
)
|
||||
|
||||
notificationService.createNotificationChannelsForAccount(accountEntity)
|
||||
|
||||
runInBackground {
|
||||
val notification = NotificationHelper.make(
|
||||
context,
|
||||
notificationManager,
|
||||
val notification = notificationService.createBaseNotification(
|
||||
Notification(
|
||||
type = type,
|
||||
id = "id",
|
||||
|
|
@ -115,8 +130,7 @@ class MainActivityTest {
|
|||
status = null,
|
||||
report = null
|
||||
),
|
||||
accountEntity,
|
||||
true
|
||||
accountEntity
|
||||
)
|
||||
notificationManager.notify("id", 1, notification)
|
||||
}
|
||||
|
|
@ -128,17 +142,42 @@ class MainActivityTest {
|
|||
private fun startMainActivity(intent: Intent): Activity {
|
||||
val controller = Robolectric.buildActivity(MainActivity::class.java, intent)
|
||||
val activity = controller.get()
|
||||
activity.eventHub = EventHub()
|
||||
activity.accountManager = mock {
|
||||
val eventHub = EventHub()
|
||||
activity.eventHub = eventHub
|
||||
val accountManager: AccountManager = mock {
|
||||
on { accounts } doReturn listOf(accountEntity)
|
||||
on { accountsFlow } doReturn MutableStateFlow(listOf(accountEntity))
|
||||
on { activeAccount } doReturn accountEntity
|
||||
}
|
||||
activity.draftsAlert = mock {}
|
||||
activity.shareShortcutHelper = mock {}
|
||||
activity.externalScope = TestScope()
|
||||
activity.mastodonApi = mock {
|
||||
activity.accountManager = accountManager
|
||||
activity.draftsAlert = mock { }
|
||||
val api: MastodonApi = mock {
|
||||
onBlocking { accountVerifyCredentials() } doReturn NetworkResult.success(account)
|
||||
onBlocking { listAnnouncements(false) } doReturn NetworkResult.success(emptyList())
|
||||
onBlocking { announcements() } doReturn NetworkResult.success(emptyList())
|
||||
}
|
||||
activity.mastodonApi = api
|
||||
activity.preferences = mock(defaultAnswer = {
|
||||
when (it.method.returnType) {
|
||||
String::class.java -> "test"
|
||||
Boolean::class.java -> false
|
||||
else -> null
|
||||
}
|
||||
})
|
||||
val viewModel = MainViewModel(
|
||||
context = mock {
|
||||
on { getSystemService(Context.NOTIFICATION_SERVICE) } doReturn mock<NotificationManager>()
|
||||
},
|
||||
api = api,
|
||||
eventHub = eventHub,
|
||||
accountManager = accountManager,
|
||||
shareShortcutHelper = mock(),
|
||||
notificationService = mock(),
|
||||
)
|
||||
val testViewModelFactory = viewModelFactory {
|
||||
initializer { viewModel }
|
||||
}
|
||||
activity.viewModelProviderFactory = testViewModelFactory
|
||||
|
||||
controller.create().start()
|
||||
return activity
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,181 +1,191 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.text.Spannable
|
||||
import com.keylesspalace.tusky.util.FoundMatchType
|
||||
import com.keylesspalace.tusky.util.MENTION_PATTERN_STRING
|
||||
import com.keylesspalace.tusky.util.PatternFinder
|
||||
import com.keylesspalace.tusky.util.TAG_PATTERN_STRING
|
||||
import com.keylesspalace.tusky.util.highlightSpans
|
||||
import org.junit.Assert
|
||||
import com.keylesspalace.tusky.util.twittertext.Regex
|
||||
import java.util.regex.Pattern
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
class SpanUtilsTest {
|
||||
@Test
|
||||
fun matchesMixedSpans() {
|
||||
val input = "one #one two: @two three : https://thr.ee/meh?foo=bar&wat=@at#hmm four #four five @five ろく#six"
|
||||
val inputSpannable = FakeSpannable(input)
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(6, spans.size)
|
||||
/** The [Pattern.UNICODE_CHARACTER_CLASS] flag is not supported on Android, on Android it is just always on.
|
||||
* Since thesse tests run on a regular Jvm, we need a to set this flag or they would behave differently.
|
||||
* */
|
||||
private val urlPattern = Regex.VALID_URL_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CHARACTER_CLASS)
|
||||
private val tagPattern = TAG_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CHARACTER_CLASS)
|
||||
private val mentionPattern = MENTION_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CHARACTER_CLASS)
|
||||
|
||||
val finders = listOf(
|
||||
PatternFinder("http", FoundMatchType.HTTPS_URL, urlPattern),
|
||||
PatternFinder("#", FoundMatchType.TAG, tagPattern),
|
||||
PatternFinder("@", FoundMatchType.MENTION, mentionPattern)
|
||||
)
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class SpanUtilsTest(
|
||||
private val stringToHighlight: String,
|
||||
private val highlights: List<Pair<Int, Int>>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data() = listOf(
|
||||
arrayOf("@mention", listOf(0 to 8)),
|
||||
arrayOf("@mention@server.com", listOf(0 to 19)),
|
||||
arrayOf("#tag", listOf(0 to 4)),
|
||||
arrayOf("#tåg", listOf(0 to 4)),
|
||||
arrayOf("https://thr.ee/meh?foo=bar&wat=@at#hmm", listOf(0 to 38)),
|
||||
arrayOf("http://thr.ee/meh?foo=bar&wat=@at#hmm", listOf(0 to 37)),
|
||||
arrayOf(
|
||||
"one #one two: @two three : https://thr.ee/meh?foo=bar&wat=@at#hmm four #four five @five 6 #six",
|
||||
listOf(4 to 8, 14 to 18, 27 to 65, 71 to 76, 82 to 87, 90 to 94)
|
||||
),
|
||||
arrayOf("http://first.link https://second.link", listOf(0 to 17, 18 to 37)),
|
||||
arrayOf("#test", listOf(0 to 5)),
|
||||
arrayOf(" #AfterSpace", listOf(1 to 12)),
|
||||
arrayOf("#BeforeSpace ", listOf(0 to 12)),
|
||||
arrayOf("@#after_at", listOf(1 to 10)),
|
||||
arrayOf("あいうえお#after_hiragana", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("##DoubleHash", listOf(1 to 12)),
|
||||
arrayOf("###TripleHash", listOf(2 to 13)),
|
||||
arrayOf("something#notAHashtag", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("test##maybeAHashtag", listOf(5 to 19)),
|
||||
arrayOf("testhttp://not.a.url.com", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("test@notAMention", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("test@notAMention#notAHashtag", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("test@notAMention@server.com", listOf<Pair<Int, Int>>()),
|
||||
// Mastodon will not highlight this mention, although it would be valid according to their regex
|
||||
// arrayOf("@test@notAMention@server.com", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("testhttps://not.a.url.com", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("#hashtag1", listOf(0 to 9)),
|
||||
arrayOf("#1hashtag", listOf(0 to 9)),
|
||||
arrayOf("#サイクリング", listOf(0 to 7)),
|
||||
arrayOf("#自転車に乗る", listOf(0 to 7)),
|
||||
arrayOf("(#test)", listOf(1 to 6)),
|
||||
arrayOf(")#test(", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("{#test}", listOf(1 to 6)),
|
||||
arrayOf("[#test]", listOf(1 to 6)),
|
||||
arrayOf("}#test{", listOf(1 to 6)),
|
||||
arrayOf("]#test[", listOf(1 to 6)),
|
||||
arrayOf("<#test>", listOf(1 to 6)),
|
||||
arrayOf(">#test<", listOf(1 to 6)),
|
||||
arrayOf("((#Test))", listOf(2 to 7)),
|
||||
arrayOf("((##Te)st)", listOf(3 to 6)),
|
||||
arrayOf("[@ConnyDuck]", listOf(1 to 11)),
|
||||
arrayOf("(@ConnyDuck)", listOf(1 to 11)),
|
||||
arrayOf("(@ConnyDuck@chaos.social)", listOf(1 to 24)),
|
||||
arrayOf("Test(https://test.xyz/blubb(test)))))))))))", listOf(5 to 33)),
|
||||
arrayOf("Test https://test.xyz/blubb(test)))))))))))", listOf(5 to 33)),
|
||||
arrayOf("Test https://test.xyz/blubbtest)))))))))))", listOf(5 to 31)),
|
||||
arrayOf("#https://test.com", listOf(0 to 6)),
|
||||
arrayOf("#https://t", listOf(0 to 6)),
|
||||
arrayOf("(https://blubb.com", listOf(1 to 18)),
|
||||
arrayOf("https://example.com/path#anchor", listOf(0 to 31)),
|
||||
arrayOf("test httpx2345://wrong.protocol.com", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("test https://nonexistent.topleveldomain.testtest", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("test https://example.com:1234 domain with port", listOf(5 to 29)),
|
||||
arrayOf("http://1.1.1.1", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("http://foo.bar/?q=Test%20URL-encoded%20stuff", listOf(0 to 44)),
|
||||
arrayOf("http://userid:password@example.com", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("http://userid@example.com", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("http://foo.com/blah_blah_(brackets)_(again)", listOf(0 to 43)),
|
||||
arrayOf("test example.com/no/protocol", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("protocol only https://", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("no tld https://test", listOf<Pair<Int, Int>>()),
|
||||
arrayOf("mention in url https://test.com/@test@domain.cat", listOf(15 to 48)),
|
||||
arrayOf("#hash_tag", listOf(0 to 9)),
|
||||
arrayOf("#hashtag_", listOf(0 to 9)),
|
||||
arrayOf("#hashtag_#tag", listOf(0 to 9, 9 to 13)),
|
||||
arrayOf("#hash_tag#tag", listOf(0 to 9)),
|
||||
arrayOf("_#hashtag", listOf(1 to 9)),
|
||||
arrayOf("@@ConnyDuck@chaos.social", listOf(1 to 24)),
|
||||
arrayOf("http://https://connyduck.at", listOf(7 to 27)),
|
||||
arrayOf("https://https://connyduck.at", listOf(8 to 28)),
|
||||
arrayOf("http:// http://connyduck.at", listOf(8 to 27)),
|
||||
arrayOf("https:// https://connyduck.at", listOf(9 to 29)),
|
||||
arrayOf("https:// #test https://connyduck.at", listOf(9 to 14, 15 to 35)),
|
||||
arrayOf("http:// @connyduck http://connyduck.at", listOf(8 to 18, 19 to 38)),
|
||||
// emojis count as multiple characters
|
||||
arrayOf("😜https://connyduck.at", listOf(2 to 22)),
|
||||
arrayOf("😜#tag", listOf(2 to 6)),
|
||||
arrayOf("😜@user@mastodon.example", listOf(2 to 24)),
|
||||
// case should be ignored on protocols https://github.com/tuskyapp/Tusky/issues/4641
|
||||
arrayOf("HTTPS://example.com", listOf(0 to 19)),
|
||||
arrayOf("Http://example.com/test", listOf(0 to 23)),
|
||||
arrayOf("test Https://example.com/test", listOf(5 to 29)),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesntMergeAdjacentURLs() {
|
||||
val firstURL = "http://first.thing"
|
||||
val secondURL = "https://second.thing"
|
||||
val inputSpannable = FakeSpannable("$firstURL $secondURL")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(2, spans.size)
|
||||
Assert.assertEquals(firstURL.length, spans[0].end - spans[0].start)
|
||||
Assert.assertEquals(secondURL.length, spans[1].end - spans[1].start)
|
||||
}
|
||||
fun testHighlighting() {
|
||||
val inputSpannable = FakeSpannable(stringToHighlight)
|
||||
inputSpannable.highlightSpans(0xffffff, finders)
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class MatchingTests(private val thingToHighlight: String) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
"@mention",
|
||||
"#tag",
|
||||
"#tåg",
|
||||
"https://thr.ee/meh?foo=bar&wat=@at#hmm",
|
||||
"http://thr.ee/meh?foo=bar&wat=@at#hmm"
|
||||
)
|
||||
assertEquals(highlights.size, inputSpannable.spans.size)
|
||||
|
||||
inputSpannable.spans
|
||||
.sortedBy { span -> span.start }
|
||||
.forEachIndexed { index, span ->
|
||||
assertEquals(highlights[index].first, span.start)
|
||||
assertEquals(highlights[index].second, span.end)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSpanAtStart() {
|
||||
val inputSpannable = FakeSpannable(thingToHighlight)
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
Assert.assertEquals(thingToHighlight.length, spans[0].end - spans[0].start)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchesSpanNotAtStart() {
|
||||
val inputSpannable = FakeSpannable(" $thingToHighlight")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
Assert.assertEquals(thingToHighlight.length, spans[0].end - spans[0].start)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotMatchSpanEmbeddedInText() {
|
||||
val inputSpannable = FakeSpannable("aa${thingToHighlight}aa")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertTrue(spans.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotMatchSpanEmbeddedInAnotherSpan() {
|
||||
val inputSpannable = FakeSpannable("@aa${thingToHighlight}aa")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spansDoNotOverlap() {
|
||||
val begin = "@begin"
|
||||
val end = "#end"
|
||||
val inputSpannable = FakeSpannable("$begin $thingToHighlight $end")
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(3, spans.size)
|
||||
|
||||
val middleSpan = spans.single { span -> span.start > 0 && span.end < inputSpannable.lastIndex }
|
||||
Assert.assertEquals(begin.length + 1, middleSpan.start)
|
||||
Assert.assertEquals(inputSpannable.length - end.length - 1, middleSpan.end)
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class HighlightingTestsForTag(
|
||||
private val text: String,
|
||||
private val expectedStartIndex: Int,
|
||||
private val expectedEndIndex: Int
|
||||
) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
return listOf(
|
||||
arrayOf("#test", 0, 5),
|
||||
arrayOf(" #AfterSpace", 1, 12),
|
||||
arrayOf("#BeforeSpace ", 0, 12),
|
||||
arrayOf("@#after_at", 1, 10),
|
||||
arrayOf("あいうえお#after_hiragana", 5, 20),
|
||||
arrayOf("##DoubleHash", 1, 12),
|
||||
arrayOf("###TripleHash", 2, 13)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matchExpectations() {
|
||||
val inputSpannable = FakeSpannable(text)
|
||||
highlightSpans(inputSpannable, 0xffffff)
|
||||
val spans = inputSpannable.spans
|
||||
Assert.assertEquals(1, spans.size)
|
||||
val span = spans.first()
|
||||
Assert.assertEquals(expectedStartIndex, span.start)
|
||||
Assert.assertEquals(expectedEndIndex, span.end)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeSpannable(private val text: String) : Spannable {
|
||||
val spans = mutableListOf<BoundedSpan>()
|
||||
|
||||
override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) {
|
||||
spans.add(BoundedSpan(what, start, end))
|
||||
}
|
||||
|
||||
override fun <T : Any> getSpans(start: Int, end: Int, type: Class<T>): Array<T> {
|
||||
return spans.filter { it.start >= start && it.end <= end && type.isInstance(it.span) }
|
||||
.map { it.span }
|
||||
.toTypedArray() as Array<T>
|
||||
}
|
||||
|
||||
override fun removeSpan(what: Any?) {
|
||||
spans.removeIf { span -> span.span == what }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return text
|
||||
}
|
||||
|
||||
override val length: Int
|
||||
get() = text.length
|
||||
|
||||
class BoundedSpan(val span: Any?, val start: Int, val end: Int)
|
||||
|
||||
override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanEnd(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanFlags(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun get(index: Int): Char {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanStart(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeSpannable(private val text: String) : Spannable {
|
||||
val spans = mutableListOf<BoundedSpan>()
|
||||
|
||||
override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) {
|
||||
spans.add(BoundedSpan(what, start, end))
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> getSpans(start: Int, end: Int, type: Class<T>): Array<T> {
|
||||
return spans.filter { it.start >= start && it.end <= end && type.isInstance(it.span) }
|
||||
.map { it.span }
|
||||
.toTypedArray() as Array<T>
|
||||
}
|
||||
|
||||
override fun removeSpan(what: Any?) {
|
||||
spans.removeIf { span -> span.span == what }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return text
|
||||
}
|
||||
|
||||
override val length: Int
|
||||
get() = text.length
|
||||
|
||||
class BoundedSpan(val span: Any?, val start: Int, val end: Int)
|
||||
|
||||
override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanEnd(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getSpanFlags(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun get(index: Int): Char {
|
||||
return text[index]
|
||||
}
|
||||
|
||||
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
|
||||
return text.subSequence(startIndex, endIndex)
|
||||
}
|
||||
|
||||
override fun getSpanStart(tag: Any?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StatusComparisonTest {
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,20 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import com.keylesspalace.tusky.di.PreferencesEntryPoint
|
||||
import dagger.hilt.internal.GeneratedComponent
|
||||
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
||||
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff
|
||||
class TuskyApplication : Application() {
|
||||
class TuskyApplication : Application(), PreferencesEntryPoint, GeneratedComponent {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this))
|
||||
}
|
||||
|
||||
override fun preferences(): SharedPreferences = mock {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,28 +20,31 @@ package com.keylesspalace.tusky.components.compose
|
|||
import android.content.Intent
|
||||
import android.os.Looper.getMainLooper
|
||||
import android.widget.EditText
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.EmojisEntity
|
||||
import com.keylesspalace.tusky.db.InstanceDao
|
||||
import com.keylesspalace.tusky.db.InstanceInfoEntity
|
||||
import com.keylesspalace.tusky.db.dao.InstanceDao
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.db.entity.EmojisEntity
|
||||
import com.keylesspalace.tusky.db.entity.InstanceInfoEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.entity.InstanceConfiguration
|
||||
import com.keylesspalace.tusky.entity.InstanceV1
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.StatusConfiguration
|
||||
import com.keylesspalace.tusky.finders
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.squareup.moshi.adapter
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
@ -53,7 +56,6 @@ import org.junit.runner.RunWith
|
|||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
|
|
@ -66,7 +68,7 @@ import retrofit2.Response
|
|||
* Created by charlag on 3/7/18.
|
||||
*/
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ComposeActivityTest {
|
||||
private lateinit var activity: ComposeActivity
|
||||
|
|
@ -107,6 +109,8 @@ class ComposeActivityTest {
|
|||
activity = controller.get()
|
||||
|
||||
accountManagerMock = mock {
|
||||
on { accounts } doReturn listOf(account)
|
||||
on { accountsFlow } doReturn MutableStateFlow(listOf(account))
|
||||
on { activeAccount } doReturn account
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +160,22 @@ class ComposeActivityTest {
|
|||
putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions)
|
||||
}
|
||||
|
||||
val viewModelFactoryMock: ViewModelFactory = mock {
|
||||
on { create(eq(ComposeViewModel::class.java), any()) } doReturn viewModel
|
||||
val testViewModelFactory = viewModelFactory {
|
||||
initializer { viewModel }
|
||||
}
|
||||
|
||||
activity.accountManager = accountManagerMock
|
||||
activity.viewModelFactory = viewModelFactoryMock
|
||||
activity.viewModelProviderFactory = testViewModelFactory
|
||||
|
||||
activity.preferences = mock(defaultAnswer = {
|
||||
when (it.method.returnType) {
|
||||
String::class.java -> "test"
|
||||
Boolean::class.java -> false
|
||||
else -> null
|
||||
}
|
||||
})
|
||||
|
||||
activity.highlightFinders = finders
|
||||
|
||||
controller.create().start()
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
|
@ -277,7 +291,7 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
insertSomeTextInContent(additionalContent + url)
|
||||
assertEquals(additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength())
|
||||
|
|
@ -286,7 +300,7 @@ class ComposeActivityTest {
|
|||
@Test
|
||||
fun whenTextContainsShortUrls_allUrlsGetEllipsized() {
|
||||
val shortUrl = "https://tusky.app"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||
assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength())
|
||||
|
|
@ -294,7 +308,7 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
insertSomeTextInContent(url + additionalContent + url)
|
||||
assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength())
|
||||
|
|
@ -302,7 +316,7 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfiguration() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
|
||||
|
|
@ -314,7 +328,7 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfigurationV1() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = "Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
|
|
@ -327,7 +341,7 @@ class ComposeActivityTest {
|
|||
@Test
|
||||
fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfiguration() {
|
||||
val shortUrl = "https://tusky.app"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 18 // The intention is that this is longer than shortUrl.length
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
|
||||
|
|
@ -340,7 +354,7 @@ class ComposeActivityTest {
|
|||
@Test
|
||||
fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfigurationV1() {
|
||||
val shortUrl = "https://tusky.app"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 18 // The intention is that this is longer than shortUrl.length
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
|
|
@ -352,7 +366,7 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfiguration() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) }
|
||||
|
|
@ -364,7 +378,7 @@ class ComposeActivityTest {
|
|||
|
||||
@Test
|
||||
fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfigurationV1() {
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM"
|
||||
val additionalContent = " Check out this @image #search result: "
|
||||
val customUrlLength = 16
|
||||
instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) }
|
||||
|
|
@ -585,7 +599,7 @@ class ComposeActivityTest {
|
|||
private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration {
|
||||
return Instance.Configuration(
|
||||
Instance.Configuration.Urls(),
|
||||
Instance.Configuration.Accounts(1),
|
||||
Instance.Configuration.Accounts(maxFeaturedTags = 1, maxProfileFields = 4),
|
||||
Instance.Configuration.Statuses(
|
||||
maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT,
|
||||
InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
|
@ -30,7 +30,7 @@ class ComposeTokenizerTest(
|
|||
companion object {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
fun data(): Iterable<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf("@mention", 0, 8),
|
||||
arrayOf("@ment10n", 0, 8),
|
||||
|
|
@ -89,7 +89,7 @@ class ComposeTokenizerTest(
|
|||
|
||||
@Test
|
||||
fun tokenIndices_matchExpectations() {
|
||||
Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
|
||||
Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
|
||||
assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length))
|
||||
assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.settings.DefaultReplyVisibility
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ComposeViewModelTest {
|
||||
|
||||
private lateinit var api: MastodonApi
|
||||
private lateinit var accountManager: AccountManager
|
||||
private lateinit var eventHub: EventHub
|
||||
private lateinit var viewModel: ComposeViewModel
|
||||
|
||||
private fun setup(defaultReplyVisibility: DefaultReplyVisibility = DefaultReplyVisibility.UNLISTED) {
|
||||
api = mock()
|
||||
accountManager = mock {
|
||||
on { activeAccount } doReturn
|
||||
AccountEntity(
|
||||
id = 1,
|
||||
domain = "test.domain",
|
||||
accessToken = "fakeToken",
|
||||
clientId = "fakeId",
|
||||
clientSecret = "fakeSecret",
|
||||
isActive = true,
|
||||
defaultReplyPrivacy = defaultReplyVisibility
|
||||
)
|
||||
}
|
||||
eventHub = EventHub()
|
||||
|
||||
viewModel = ComposeViewModel(
|
||||
api = api,
|
||||
accountManager = accountManager,
|
||||
mediaUploader = mock(),
|
||||
serviceClient = mock(),
|
||||
draftHelper = mock(),
|
||||
instanceInfoRepo = mock(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startingVisibility initially set to defaultPostPrivacy for post`() {
|
||||
setup()
|
||||
viewModel.setup(null)
|
||||
|
||||
assertEquals(Status.Visibility.PUBLIC, viewModel.statusVisibility.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startingVisibility initially set to replyPostPrivacy for reply`() {
|
||||
setup()
|
||||
viewModel.setup(ComposeActivity.ComposeOptions(inReplyToId = "123"))
|
||||
|
||||
assertEquals(Status.Visibility.UNLISTED, viewModel.statusVisibility.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startingVisibility initially set to defaultPostPrivacy when replyPostPrivacy is MATCH_DEFAULT_POST_VISIBILITY for reply`() {
|
||||
setup(defaultReplyVisibility = DefaultReplyVisibility.MATCH_DEFAULT_POST_VISIBILITY)
|
||||
viewModel.setup(ComposeActivity.ComposeOptions(inReplyToId = "123"))
|
||||
|
||||
assertEquals(Status.Visibility.PUBLIC, viewModel.statusVisibility.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import com.keylesspalace.tusky.SpanUtilsTest
|
||||
import com.keylesspalace.tusky.FakeSpannable
|
||||
import com.keylesspalace.tusky.finders
|
||||
import com.keylesspalace.tusky.util.highlightSpans
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
|
@ -26,7 +27,7 @@ import org.robolectric.ParameterizedRobolectricTestRunner
|
|||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
@Config(sdk = [34])
|
||||
class StatusLengthTest(
|
||||
private val text: String,
|
||||
private val expectedLength: Int
|
||||
|
|
@ -34,7 +35,7 @@ class StatusLengthTest(
|
|||
companion object {
|
||||
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
fun data(): Iterable<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf("", 0),
|
||||
arrayOf(" ", 1),
|
||||
|
|
@ -42,22 +43,28 @@ class StatusLengthTest(
|
|||
arrayOf("🫣", 1),
|
||||
// "@user@server" should be treated as "@user"
|
||||
arrayOf("123 @example@example.org", 12),
|
||||
// URLs under 23 chars are treated as 23 chars
|
||||
arrayOf("123 http://example.url", 27),
|
||||
// URLs are always treated as 23 even if they are shorter
|
||||
arrayOf("123 http://example.org", 27),
|
||||
// URLs over 23 chars are treated as 23 chars
|
||||
arrayOf("123 http://urlthatislongerthan23characters.example.org", 27),
|
||||
// Short hashtags are treated as is
|
||||
arrayOf("123 #basictag", 13),
|
||||
// Long hashtags are *also* treated as is (not treated as 23, like URLs)
|
||||
arrayOf("123 #atagthatislongerthan23characters", 37)
|
||||
arrayOf("123 #atagthatislongerthan23characters", 37),
|
||||
// urls can have balanced parenthesis, otherwise they are ignored https://github.com/tuskyapp/Tusky/issues/4425
|
||||
arrayOf("(https://en.wikipedia.org/wiki/Beethoven_(horse))", 25),
|
||||
// protocols can have any case https://github.com/tuskyapp/Tusky/issues/4641
|
||||
arrayOf("Http://example.org", 23),
|
||||
arrayOf("HTTPS://example.org", 23),
|
||||
arrayOf("HTTPS://EXAMPLE.ORG", 23)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusLength_matchesExpectations() {
|
||||
val spannedText = SpanUtilsTest.FakeSpannable(text)
|
||||
highlightSpans(spannedText, 0)
|
||||
val spannedText = FakeSpannable(text)
|
||||
spannedText.highlightSpans(0, finders)
|
||||
|
||||
assertEquals(
|
||||
expectedLength,
|
||||
|
|
@ -67,10 +74,10 @@ class StatusLengthTest(
|
|||
|
||||
@Test
|
||||
fun statusLength_withCwText_matchesExpectations() {
|
||||
val spannedText = SpanUtilsTest.FakeSpannable(text)
|
||||
highlightSpans(spannedText, 0)
|
||||
val spannedText = FakeSpannable(text)
|
||||
spannedText.highlightSpans(0, finders)
|
||||
|
||||
val cwText = SpanUtilsTest.FakeSpannable(
|
||||
val cwText = FakeSpannable(
|
||||
"a @example@example.org #hashtagmention and http://example.org URL"
|
||||
)
|
||||
assertEquals(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.withTransaction
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.fakeAccount
|
||||
import com.keylesspalace.tusky.components.timeline.fakeStatus
|
||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.entity.NotificationDataEntity
|
||||
import com.keylesspalace.tusky.db.entity.NotificationEntity
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.Report
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import java.util.Date
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
fun fakeNotification(
|
||||
type: Notification.Type = Notification.Type.Favourite,
|
||||
id: String = "1",
|
||||
account: TimelineAccount = fakeAccount(id = id),
|
||||
status: Status? = fakeStatus(id = id),
|
||||
report: Report? = null
|
||||
) = Notification(
|
||||
type = type,
|
||||
id = id,
|
||||
account = account,
|
||||
status = status,
|
||||
report = report
|
||||
)
|
||||
|
||||
fun fakeReport(
|
||||
id: String = "1",
|
||||
category: String = "spam",
|
||||
statusIds: List<String>? = null,
|
||||
createdAt: Date = Date(1712509983273),
|
||||
targetAccount: TimelineAccount = fakeAccount()
|
||||
) = Report(
|
||||
id = id,
|
||||
category = category,
|
||||
statusIds = statusIds,
|
||||
createdAt = createdAt,
|
||||
targetAccount = targetAccount
|
||||
)
|
||||
|
||||
fun Notification.toNotificationDataEntity(
|
||||
tuskyAccountId: Long,
|
||||
isStatusExpanded: Boolean = false,
|
||||
isStatusContentShowing: Boolean = false
|
||||
) = NotificationDataEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
type = type,
|
||||
id = id,
|
||||
account = account.toEntity(tuskyAccountId),
|
||||
status = status?.toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
expanded = isStatusExpanded,
|
||||
contentShowing = isStatusContentShowing,
|
||||
contentCollapsed = true
|
||||
),
|
||||
statusAccount = status?.account?.toEntity(tuskyAccountId),
|
||||
report = report?.toEntity(tuskyAccountId),
|
||||
reportTargetAccount = report?.targetAccount?.toEntity(tuskyAccountId),
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
)
|
||||
|
||||
fun Placeholder.toNotificationDataEntity(
|
||||
tuskyAccountId: Long
|
||||
) = NotificationDataEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
type = null,
|
||||
id = id,
|
||||
account = null,
|
||||
status = null,
|
||||
statusAccount = null,
|
||||
report = null,
|
||||
reportTargetAccount = null,
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
)
|
||||
|
||||
suspend fun AppDatabase.insert(notifications: List<Notification>, tuskyAccountId: Long = 1) = withTransaction {
|
||||
notifications.forEach { notification ->
|
||||
|
||||
timelineAccountDao().insert(
|
||||
notification.account.toEntity(tuskyAccountId)
|
||||
)
|
||||
|
||||
notification.report?.let { report ->
|
||||
timelineAccountDao().insert(
|
||||
report.targetAccount.toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
)
|
||||
)
|
||||
notificationsDao().insertReport(report.toEntity(tuskyAccountId))
|
||||
}
|
||||
notification.status?.let { status ->
|
||||
timelineAccountDao().insert(
|
||||
status.account.toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
)
|
||||
)
|
||||
timelineStatusDao().insert(
|
||||
status.toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
expanded = false,
|
||||
contentShowing = false,
|
||||
contentCollapsed = true
|
||||
)
|
||||
)
|
||||
}
|
||||
notificationsDao().insertNotification(
|
||||
NotificationEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
type = notification.type,
|
||||
id = notification.id,
|
||||
accountId = notification.account.id,
|
||||
statusId = notification.status?.id,
|
||||
reportId = notification.report?.id,
|
||||
event = null,
|
||||
moderationWarning = null,
|
||||
loading = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun AppDatabase.assertNotifications(
|
||||
expected: List<NotificationDataEntity>,
|
||||
tuskyAccountId: Long = 1
|
||||
) {
|
||||
val pagingSource = notificationsDao().getNotifications(tuskyAccountId)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false))
|
||||
|
||||
val loaded = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(expected, loaded)
|
||||
}
|
||||
|
|
@ -0,0 +1,572 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.os.Looper.getMainLooper
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.fakeStatus
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.db.entity.NotificationDataEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.After
|
||||
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.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.doThrow
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NotificationsRemoteMediatorTest {
|
||||
|
||||
private val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.example",
|
||||
accessToken = "token",
|
||||
clientId = "id",
|
||||
clientSecret = "secret",
|
||||
isActive = true
|
||||
)
|
||||
|
||||
private val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doReturn account
|
||||
on { accountsFlow } doReturn MutableStateFlow(listOf(account))
|
||||
}
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setup() {
|
||||
shadowOf(getMainLooper()).idle()
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
@ExperimentalCoroutinesApi
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should return error when network call returns error code`() = runTest {
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state())
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Error)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException)
|
||||
assertEquals(500, (result.throwable as HttpException).code())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should return error when network call fails`() = runTest {
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state())
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Error)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not prepend notifications`() = runTest {
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock(),
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1)
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = 1
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.PREPEND, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and insert placeholder when a whole page with no overlap to existing notifications is loaded`() = runTest {
|
||||
val notificationsAlreadyInDb = listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
|
||||
db.insert(notificationsAlreadyInDb)
|
||||
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "8"),
|
||||
fakeNotification(id = "7"),
|
||||
fakeNotification(id = "5")
|
||||
)
|
||||
)
|
||||
onBlocking { notifications(maxId = "3", limit = 3, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
pages = listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) },
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
),
|
||||
pageSize = 3
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(id = "8").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "7").toNotificationDataEntity(1),
|
||||
Placeholder(id = "5", loading = false).toNotificationDataEntity(1),
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "2").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and not insert placeholder when less than a whole page is loaded`() = runTest {
|
||||
val notificationsAlreadyInDb = listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
|
||||
db.insert(notificationsAlreadyInDb)
|
||||
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
// testing for https://github.com/tuskyapp/Tusky/issues/4563
|
||||
fakeNotification(
|
||||
id = "8",
|
||||
status = fakeStatus(
|
||||
id = "r1",
|
||||
reblog = fakeStatus(
|
||||
id = "8",
|
||||
authorServerId = "r1"
|
||||
)
|
||||
)
|
||||
),
|
||||
fakeNotification(id = "7"),
|
||||
fakeNotification(id = "5")
|
||||
)
|
||||
)
|
||||
onBlocking { notifications(maxId = "3", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
pages = listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) },
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(
|
||||
id = "8",
|
||||
status = fakeStatus(
|
||||
id = "8",
|
||||
authorServerId = "r1"
|
||||
)
|
||||
).toNotificationDataEntity(1),
|
||||
fakeNotification(id = "7").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "5").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "2").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and not insert placeholders when there is overlap with existing notifications`() = runTest {
|
||||
val notificationsAlreadyInDb = listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
|
||||
db.insert(notificationsAlreadyInDb)
|
||||
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "6"),
|
||||
fakeNotification(id = "4"),
|
||||
fakeNotification(id = "3")
|
||||
)
|
||||
)
|
||||
onBlocking { notifications(maxId = "3", limit = 3, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) },
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
),
|
||||
pageSize = 3
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(id = "6").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "4").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "2").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not try to refresh already cached notifications when db is empty`() = runTest {
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "5"),
|
||||
fakeNotification(id = "4"),
|
||||
fakeNotification(id = "3")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = emptyList(),
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(id = "5").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "4").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should remove deleted notification from db and keep state of statuses in the remaining ones`() = runTest {
|
||||
val notificationsAlreadyInDb = listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
db.insert(notificationsAlreadyInDb)
|
||||
|
||||
db.timelineStatusDao().setExpanded(1, "3", true)
|
||||
db.timelineStatusDao().setExpanded(1, "2", true)
|
||||
db.timelineStatusDao().setContentCollapsed(1, "1", false)
|
||||
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(emptyList())
|
||||
|
||||
onBlocking { notifications(maxId = "3", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1, isStatusExpanded = true),
|
||||
fakeNotification(id = "2").toNotificationDataEntity(1, isStatusExpanded = true),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1, isStatusContentShowing = true)
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1, isStatusExpanded = true),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1, isStatusContentShowing = true)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not remove placeholder in timeline`() = runTest {
|
||||
val notificationsAlreadyInDb = listOf(
|
||||
fakeNotification(id = "8"),
|
||||
fakeNotification(id = "7"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
db.insert(notificationsAlreadyInDb)
|
||||
|
||||
val placeholder = Placeholder(id = "6", loading = false).toNotificationEntity(1)
|
||||
db.notificationsDao().insertNotification(placeholder)
|
||||
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(sinceId = "6", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "9"),
|
||||
fakeNotification(id = "8"),
|
||||
fakeNotification(id = "7")
|
||||
)
|
||||
)
|
||||
onBlocking { notifications(maxId = "8", sinceId = "6", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "8"),
|
||||
fakeNotification(id = "7")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) },
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertFalse((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(id = "9").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "8").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "7").toNotificationDataEntity(1),
|
||||
Placeholder(id = "6", loading = false).toNotificationDataEntity(1),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should append notifications`() = runTest {
|
||||
val notificationsAlreadyInDb = listOf(
|
||||
fakeNotification(id = "8"),
|
||||
fakeNotification(id = "7"),
|
||||
fakeNotification(id = "5")
|
||||
)
|
||||
|
||||
db.insert(notificationsAlreadyInDb)
|
||||
|
||||
val remoteMediator = NotificationsRemoteMediator(
|
||||
viewModel = mockViewModel(),
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(maxId = "5", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) },
|
||||
prevKey = null,
|
||||
nextKey = 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = remoteMediator.load(LoadType.APPEND, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
db.assertNotifications(
|
||||
listOf(
|
||||
fakeNotification(id = "8").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "7").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "5").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "2").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "1").toNotificationDataEntity(1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun state(
|
||||
pages: List<PagingSource.LoadResult.Page<Int, NotificationDataEntity>> = emptyList(),
|
||||
pageSize: Int = 20
|
||||
) = PagingState(
|
||||
pages = pages,
|
||||
anchorPosition = null,
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize
|
||||
),
|
||||
leadingPlaceholderCount = 0
|
||||
)
|
||||
|
||||
private fun mockViewModel(): NotificationsViewModel {
|
||||
return mock {
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { excludes } doReturn MutableStateFlow(emptySet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,15 +11,17 @@ import androidx.room.Room
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
||||
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.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.db.entity.HomeTimelineData
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
@ -37,21 +39,10 @@ import org.robolectric.annotation.Config
|
|||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CachedTimelineRemoteMediatorTest {
|
||||
|
||||
private val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doReturn AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.example",
|
||||
accessToken = "token",
|
||||
clientId = "id",
|
||||
clientSecret = "secret",
|
||||
isActive = true
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
|
@ -75,17 +66,16 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should return error when network call returns error code`() {
|
||||
fun `should return error when network call returns error code`() = runTest {
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state())
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Error)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException)
|
||||
|
|
@ -94,17 +84,16 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should return error when network call fails`() {
|
||||
fun `should return error when network call fails`() = runTest {
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state())
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Error)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException)
|
||||
|
|
@ -112,19 +101,18 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not prepend statuses`() {
|
||||
fun `should not prepend statuses`() = runTest {
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock(),
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusEntityWithAccount("3")
|
||||
fakeHomeTimelineData("3")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = 1
|
||||
|
|
@ -132,7 +120,7 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.PREPEND, state) }
|
||||
val result = remoteMediator.load(LoadType.PREPEND, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
|
@ -140,35 +128,34 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and insert placeholder when a whole page with no overlap to existing statuses is loaded`() {
|
||||
fun `should refresh and insert placeholder when a whole page with no overlap to existing statuses is loaded`() = runTest {
|
||||
val statusesAlreadyInDb = listOf(
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
|
||||
db.insert(statusesAlreadyInDb)
|
||||
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(limit = 3) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("8"),
|
||||
mockStatus("7"),
|
||||
mockStatus("5")
|
||||
fakeStatus("8"),
|
||||
fakeStatus("7"),
|
||||
fakeStatus("5")
|
||||
)
|
||||
)
|
||||
onBlocking { homeTimeline(maxId = "3", limit = 3) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -182,56 +169,53 @@ class CachedTimelineRemoteMediatorTest {
|
|||
pageSize = 3
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("8"),
|
||||
mockStatusEntityWithAccount("7"),
|
||||
TimelineStatusWithAccount(
|
||||
status = Placeholder("5", loading = false).toEntity(1)
|
||||
),
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("8"),
|
||||
fakeHomeTimelineData("7"),
|
||||
fakePlaceholderHomeTimelineData("5"),
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and not insert placeholder when less than a whole page is loaded`() {
|
||||
fun `should refresh and not insert placeholder when less than a whole page is loaded`() = runTest {
|
||||
val statusesAlreadyInDb = listOf(
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
|
||||
db.insert(statusesAlreadyInDb)
|
||||
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("8"),
|
||||
mockStatus("7"),
|
||||
mockStatus("5")
|
||||
fakeStatus("8"),
|
||||
fakeStatus("7"),
|
||||
fakeStatus("5")
|
||||
)
|
||||
)
|
||||
onBlocking { homeTimeline(maxId = "3", limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -244,54 +228,53 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("8"),
|
||||
mockStatusEntityWithAccount("7"),
|
||||
mockStatusEntityWithAccount("5"),
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("8"),
|
||||
fakeHomeTimelineData("7"),
|
||||
fakeHomeTimelineData("5"),
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and not insert placeholders when there is overlap with existing statuses`() {
|
||||
fun `should refresh and not insert placeholders when there is overlap with existing statuses`() = runTest {
|
||||
val statusesAlreadyInDb = listOf(
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
|
||||
db.insert(statusesAlreadyInDb)
|
||||
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(limit = 3) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("6"),
|
||||
mockStatus("4"),
|
||||
mockStatus("3")
|
||||
fakeStatus("6"),
|
||||
fakeStatus("4"),
|
||||
fakeStatus("3")
|
||||
)
|
||||
)
|
||||
onBlocking { homeTimeline(maxId = "3", limit = 3) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -305,38 +288,37 @@ class CachedTimelineRemoteMediatorTest {
|
|||
pageSize = 3
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("6"),
|
||||
mockStatusEntityWithAccount("4"),
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("6"),
|
||||
fakeHomeTimelineData("4"),
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not try to refresh already cached statuses when db is empty`() {
|
||||
fun `should not try to refresh already cached statuses when db is empty`() = runTest {
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("5"),
|
||||
mockStatus("4"),
|
||||
mockStatus("3")
|
||||
fakeStatus("5"),
|
||||
fakeStatus("4"),
|
||||
fakeStatus("3")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -349,45 +331,44 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("5"),
|
||||
mockStatusEntityWithAccount("4"),
|
||||
mockStatusEntityWithAccount("3")
|
||||
fakeHomeTimelineData("5"),
|
||||
fakeHomeTimelineData("4"),
|
||||
fakeHomeTimelineData("3")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should remove deleted status from db and keep state of other cached statuses`() {
|
||||
fun `should remove deleted status from db and keep state of other cached statuses`() = runTest {
|
||||
val statusesAlreadyInDb = listOf(
|
||||
mockStatusEntityWithAccount("3", expanded = true),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1", expanded = false)
|
||||
fakeHomeTimelineData("3", expanded = true),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1", expanded = false)
|
||||
)
|
||||
|
||||
db.insert(statusesAlreadyInDb)
|
||||
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(limit = 20) } doReturn Response.success(emptyList())
|
||||
|
||||
onBlocking { homeTimeline(maxId = "3", limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -400,50 +381,49 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("3", expanded = true),
|
||||
mockStatusEntityWithAccount("1", expanded = false)
|
||||
fakeHomeTimelineData("3", expanded = true),
|
||||
fakeHomeTimelineData("1", expanded = false)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not remove placeholder in timeline`() {
|
||||
fun `should not remove placeholder in timeline`() = runTest {
|
||||
val statusesAlreadyInDb = listOf(
|
||||
mockStatusEntityWithAccount("8"),
|
||||
mockStatusEntityWithAccount("7"),
|
||||
mockPlaceholderEntityWithAccount("6"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("8"),
|
||||
fakeHomeTimelineData("7"),
|
||||
fakePlaceholderHomeTimelineData("6"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
|
||||
db.insert(statusesAlreadyInDb)
|
||||
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(sinceId = "6", limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("9"),
|
||||
mockStatus("8"),
|
||||
mockStatus("7")
|
||||
fakeStatus("9"),
|
||||
fakeStatus("8"),
|
||||
fakeStatus("7")
|
||||
)
|
||||
)
|
||||
onBlocking { homeTimeline(maxId = "8", sinceId = "6", limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("8"),
|
||||
mockStatus("7")
|
||||
fakeStatus("8"),
|
||||
fakeStatus("7")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -456,46 +436,45 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertFalse((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("9"),
|
||||
mockStatusEntityWithAccount("8"),
|
||||
mockStatusEntityWithAccount("7"),
|
||||
mockPlaceholderEntityWithAccount("6"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("9"),
|
||||
fakeHomeTimelineData("8"),
|
||||
fakeHomeTimelineData("7"),
|
||||
fakePlaceholderHomeTimelineData("6"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should append statuses`() {
|
||||
fun `should append statuses`() = runTest {
|
||||
val statusesAlreadyInDb = listOf(
|
||||
mockStatusEntityWithAccount("8"),
|
||||
mockStatusEntityWithAccount("7"),
|
||||
mockStatusEntityWithAccount("5")
|
||||
fakeHomeTimelineData("8"),
|
||||
fakeHomeTimelineData("7"),
|
||||
fakeHomeTimelineData("5")
|
||||
)
|
||||
|
||||
db.insert(statusesAlreadyInDb)
|
||||
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
viewModel = mockViewModel(),
|
||||
api = mock {
|
||||
onBlocking { homeTimeline(maxId = "5", limit = 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
)
|
||||
)
|
||||
},
|
||||
db = db,
|
||||
moshi = moshi
|
||||
)
|
||||
|
||||
val state = state(
|
||||
|
|
@ -508,24 +487,24 @@ class CachedTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
|
||||
val result = remoteMediator.load(LoadType.APPEND, state)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
|
||||
db.assertStatuses(
|
||||
db.assertTimeline(
|
||||
listOf(
|
||||
mockStatusEntityWithAccount("8"),
|
||||
mockStatusEntityWithAccount("7"),
|
||||
mockStatusEntityWithAccount("5"),
|
||||
mockStatusEntityWithAccount("3"),
|
||||
mockStatusEntityWithAccount("2"),
|
||||
mockStatusEntityWithAccount("1")
|
||||
fakeHomeTimelineData("8"),
|
||||
fakeHomeTimelineData("7"),
|
||||
fakeHomeTimelineData("5"),
|
||||
fakeHomeTimelineData("3"),
|
||||
fakeHomeTimelineData("2"),
|
||||
fakeHomeTimelineData("1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun state(
|
||||
pages: List<PagingSource.LoadResult.Page<Int, TimelineStatusWithAccount>> = emptyList(),
|
||||
pages: List<PagingSource.LoadResult.Page<Int, HomeTimelineData>> = emptyList(),
|
||||
pageSize: Int = 20
|
||||
) = PagingState(
|
||||
pages = pages,
|
||||
|
|
@ -536,40 +515,22 @@ class CachedTimelineRemoteMediatorTest {
|
|||
leadingPlaceholderCount = 0
|
||||
)
|
||||
|
||||
private fun AppDatabase.insert(statuses: List<TimelineStatusWithAccount>) {
|
||||
runBlocking {
|
||||
statuses.forEach { statusWithAccount ->
|
||||
statusWithAccount.account?.let { account ->
|
||||
timelineDao().insertAccount(account)
|
||||
}
|
||||
statusWithAccount.reblogAccount?.let { account ->
|
||||
timelineDao().insertAccount(account)
|
||||
}
|
||||
timelineDao().insertStatus(statusWithAccount.status)
|
||||
}
|
||||
private fun mockViewModel(): CachedTimelineViewModel {
|
||||
val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.example",
|
||||
accessToken = "token",
|
||||
clientId = "id",
|
||||
clientSecret = "secret",
|
||||
isActive = true
|
||||
)
|
||||
val accManager: AccountManager = mock {
|
||||
on { activeAccount } doReturn account
|
||||
on { accountsFlow } doReturn MutableStateFlow(listOf(account))
|
||||
}
|
||||
}
|
||||
|
||||
private fun AppDatabase.assertStatuses(
|
||||
expected: List<TimelineStatusWithAccount>,
|
||||
forAccount: Long = 1
|
||||
) {
|
||||
val pagingSource = timelineDao().getStatuses(forAccount)
|
||||
|
||||
val loadResult = runBlocking {
|
||||
pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false))
|
||||
}
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(expected.size, loadedStatuses.size)
|
||||
|
||||
for ((exp, prov) in expected.zip(loadedStatuses)) {
|
||||
assertEquals(exp.status, prov.status)
|
||||
if (!exp.status.isPlaceholder) {
|
||||
assertEquals(exp.account, prov.account)
|
||||
assertEquals(exp.reblogAccount, prov.reblogAccount)
|
||||
}
|
||||
return mock {
|
||||
on { accountManager } doReturn accManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.paging.PagingSource
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -12,53 +12,47 @@ import org.mockito.kotlin.doReturn
|
|||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NetworkTimelinePagingSourceTest {
|
||||
|
||||
private val status = mockStatusViewData()
|
||||
private val status = fakeStatusViewData()
|
||||
|
||||
private val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { statusData } doReturn mutableListOf(status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return empty list when params are Append`() {
|
||||
fun `should return empty list when params are Append`() = runTest {
|
||||
val pagingSource = NetworkTimelinePagingSource(timelineViewModel)
|
||||
|
||||
val params = PagingSource.LoadParams.Append("132", 20, false)
|
||||
|
||||
val expectedResult = PagingSource.LoadResult.Page(emptyList(), null, null)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(expectedResult, pagingSource.load(params))
|
||||
}
|
||||
assertEquals(expectedResult, pagingSource.load(params))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return empty list when params are Prepend`() {
|
||||
fun `should return empty list when params are Prepend`() = runTest {
|
||||
val pagingSource = NetworkTimelinePagingSource(timelineViewModel)
|
||||
|
||||
val params = PagingSource.LoadParams.Prepend("132", 20, false)
|
||||
|
||||
val expectedResult = PagingSource.LoadResult.Page(emptyList(), null, null)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(expectedResult, pagingSource.load(params))
|
||||
}
|
||||
assertEquals(expectedResult, pagingSource.load(params))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return full list when params are Refresh`() {
|
||||
fun `should return full list when params are Refresh`() = runTest {
|
||||
val pagingSource = NetworkTimelinePagingSource(timelineViewModel)
|
||||
|
||||
val params = PagingSource.LoadParams.Refresh<String>(null, 20, false)
|
||||
|
||||
val expectedResult = PagingSource.LoadResult.Page(listOf(status), null, null)
|
||||
|
||||
runBlocking {
|
||||
val result = pagingSource.load(params)
|
||||
assertEquals(expectedResult, result)
|
||||
}
|
||||
val result = pagingSource.load(params)
|
||||
assertEquals(expectedResult, result)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineRemoteMediator
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.Headers
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
@ -30,32 +31,34 @@ import org.robolectric.annotation.Config
|
|||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@Config(sdk = [29])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NetworkTimelineRemoteMediatorTest {
|
||||
|
||||
private val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.example",
|
||||
accessToken = "token",
|
||||
clientId = "id",
|
||||
clientSecret = "secret",
|
||||
isActive = true
|
||||
)
|
||||
|
||||
private val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doReturn AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.example",
|
||||
accessToken = "token",
|
||||
clientId = "id",
|
||||
clientSecret = "secret",
|
||||
isActive = true
|
||||
)
|
||||
on { activeAccount } doReturn account
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should return error when network call returns error code`() {
|
||||
fun `should return error when network call returns error code`() = runTest {
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { statusData } doReturn mutableListOf()
|
||||
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state())
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Error)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException)
|
||||
|
|
@ -64,15 +67,17 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should return error when network call fails`() {
|
||||
fun `should return error when network call fails`() = runTest {
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn mutableListOf()
|
||||
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state())
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Error)
|
||||
assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException)
|
||||
|
|
@ -80,17 +85,20 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should do initial loading`() {
|
||||
fun `should do initial loading`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf()
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn null
|
||||
onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("7"),
|
||||
mockStatus("6"),
|
||||
mockStatus("5")
|
||||
fakeStatus("7"),
|
||||
fakeStatus("6"),
|
||||
fakeStatus("5")
|
||||
),
|
||||
Headers.headersOf(
|
||||
"Link",
|
||||
|
|
@ -99,7 +107,7 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
|
|
@ -111,12 +119,12 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("6"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("6"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
verify(timelineViewModel).nextKey = "4"
|
||||
|
|
@ -127,34 +135,36 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not prepend statuses`() {
|
||||
fun `should not prepend statuses`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn "0"
|
||||
onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("5"),
|
||||
mockStatus("4"),
|
||||
mockStatus("3")
|
||||
fakeStatus("5"),
|
||||
fakeStatus("4"),
|
||||
fakeStatus("3")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "0"
|
||||
|
|
@ -162,14 +172,14 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4"),
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("5"),
|
||||
fakeStatusViewData("4"),
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
|
|
@ -179,34 +189,36 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and insert placeholder`() {
|
||||
fun `should refresh and insert placeholder`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn "0"
|
||||
onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("10"),
|
||||
mockStatus("9"),
|
||||
mockStatus("7")
|
||||
fakeStatus("10"),
|
||||
fakeStatus("9"),
|
||||
fakeStatus("7")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "0"
|
||||
|
|
@ -214,15 +226,15 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
val result = remoteMediator.load(LoadType.REFRESH, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("10"),
|
||||
mockStatusViewData("9"),
|
||||
fakeStatusViewData("10"),
|
||||
fakeStatusViewData("9"),
|
||||
StatusViewData.Placeholder("7", false),
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
|
|
@ -232,34 +244,36 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should refresh and not insert placeholders`() {
|
||||
fun `should refresh and not insert placeholders`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn "3"
|
||||
onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "3"
|
||||
|
|
@ -267,15 +281,15 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
|
||||
val result = remoteMediator.load(LoadType.APPEND, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5"),
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
|
|
@ -285,21 +299,23 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should append statuses`() {
|
||||
fun `should append statuses`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn "3"
|
||||
onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
),
|
||||
Headers.headersOf(
|
||||
"Link",
|
||||
|
|
@ -308,15 +324,15 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "3"
|
||||
|
|
@ -324,15 +340,15 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
|
||||
val result = remoteMediator.load(LoadType.APPEND, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5"),
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
verify(timelineViewModel).nextKey = "0"
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
|
|
@ -342,27 +358,29 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not append statuses when pagination end has been reached`() {
|
||||
fun `should not append statuses when pagination end has been reached`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn null
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = null
|
||||
|
|
@ -370,12 +388,12 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
|
||||
val result = remoteMediator.load(LoadType.APPEND, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
|
|
@ -385,22 +403,24 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not append duplicates for trending statuses`() {
|
||||
fun `should not append duplicates for trending statuses`() = runTest {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4"),
|
||||
mockStatusViewData("3")
|
||||
fakeStatusViewData("5"),
|
||||
fakeStatusViewData("4"),
|
||||
fakeStatusViewData("3")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { accountManager } doReturn accountManager
|
||||
on { activeAccountFlow } doReturn MutableStateFlow(account)
|
||||
on { statusData } doReturn statuses
|
||||
on { nextKey } doReturn "3"
|
||||
on { kind } doReturn TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
|
||||
onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success(
|
||||
listOf(
|
||||
mockStatus("3"),
|
||||
mockStatus("2"),
|
||||
mockStatus("1")
|
||||
fakeStatus("3"),
|
||||
fakeStatus("2"),
|
||||
fakeStatus("1")
|
||||
),
|
||||
Headers.headersOf(
|
||||
"Link",
|
||||
|
|
@ -409,7 +429,7 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
}
|
||||
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
|
||||
val remoteMediator = NetworkTimelineRemoteMediator(timelineViewModel)
|
||||
|
||||
val state = state(
|
||||
listOf(
|
||||
|
|
@ -421,14 +441,14 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
)
|
||||
)
|
||||
|
||||
val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) }
|
||||
val result = remoteMediator.load(LoadType.APPEND, state)
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4"),
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("5"),
|
||||
fakeStatusViewData("4"),
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
verify(timelineViewModel).nextKey = "5"
|
||||
assertTrue(result is RemoteMediator.MediatorResult.Success)
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import java.util.Date
|
||||
|
||||
private val fixedDate = Date(1638889052000)
|
||||
|
||||
fun mockStatus(
|
||||
id: String = "100",
|
||||
inReplyToId: String? = null,
|
||||
inReplyToAccountId: String? = null,
|
||||
spoilerText: String = "",
|
||||
reblogged: Boolean = false,
|
||||
favourited: Boolean = true,
|
||||
bookmarked: Boolean = true
|
||||
) = Status(
|
||||
id = id,
|
||||
url = "https://mastodon.example/@ConnyDuck/$id",
|
||||
account = TimelineAccount(
|
||||
id = "1",
|
||||
localUsername = "connyduck",
|
||||
username = "connyduck@mastodon.example",
|
||||
displayName = "Conny Duck",
|
||||
note = "This is their bio",
|
||||
url = "https://mastodon.example/@ConnyDuck",
|
||||
avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg"
|
||||
),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
reblog = null,
|
||||
content = "Test",
|
||||
createdAt = fixedDate,
|
||||
editedAt = null,
|
||||
emojis = emptyList(),
|
||||
reblogsCount = 1,
|
||||
favouritesCount = 2,
|
||||
repliesCount = 3,
|
||||
reblogged = reblogged,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = true,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.PUBLIC,
|
||||
attachments = ArrayList(),
|
||||
mentions = emptyList(),
|
||||
tags = emptyList(),
|
||||
application = Status.Application("Tusky", "https://tusky.app"),
|
||||
pinned = false,
|
||||
muted = false,
|
||||
poll = null,
|
||||
card = null,
|
||||
language = null,
|
||||
filtered = emptyList()
|
||||
)
|
||||
|
||||
fun mockStatusViewData(
|
||||
id: String = "100",
|
||||
inReplyToId: String? = null,
|
||||
inReplyToAccountId: String? = null,
|
||||
isDetailed: Boolean = false,
|
||||
spoilerText: String = "",
|
||||
isExpanded: Boolean = false,
|
||||
isShowingContent: Boolean = false,
|
||||
isCollapsed: Boolean = !isDetailed,
|
||||
reblogged: Boolean = false,
|
||||
favourited: Boolean = true,
|
||||
bookmarked: Boolean = true
|
||||
) = StatusViewData.Concrete(
|
||||
status = mockStatus(
|
||||
id = id,
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
spoilerText = spoilerText,
|
||||
reblogged = reblogged,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked
|
||||
),
|
||||
isExpanded = isExpanded,
|
||||
isShowingContent = isShowingContent,
|
||||
isCollapsed = isCollapsed,
|
||||
isDetailed = isDetailed
|
||||
)
|
||||
|
||||
fun mockStatusEntityWithAccount(
|
||||
id: String = "100",
|
||||
userId: Long = 1,
|
||||
expanded: Boolean = false
|
||||
): TimelineStatusWithAccount {
|
||||
val mockedStatus = mockStatus(id)
|
||||
val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
return TimelineStatusWithAccount(
|
||||
status = mockedStatus.toEntity(
|
||||
timelineUserId = userId,
|
||||
moshi = moshi,
|
||||
expanded = expanded,
|
||||
contentShowing = false,
|
||||
contentCollapsed = true
|
||||
),
|
||||
account = mockedStatus.account.toEntity(
|
||||
accountId = userId,
|
||||
moshi = moshi
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun mockPlaceholderEntityWithAccount(
|
||||
id: String,
|
||||
userId: Long = 1
|
||||
): TimelineStatusWithAccount {
|
||||
return TimelineStatusWithAccount(
|
||||
status = Placeholder(id, false).toEntity(userId)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.withTransaction
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.entity.HomeTimelineData
|
||||
import com.keylesspalace.tusky.db.entity.HomeTimelineEntity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import java.util.Date
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
private val fixedDate = Date(1638889052000)
|
||||
|
||||
fun fakeAccount(
|
||||
id: String = "100",
|
||||
domain: String = "mastodon.example"
|
||||
) = TimelineAccount(
|
||||
id = id,
|
||||
localUsername = "connyduck",
|
||||
username = "connyduck@$domain",
|
||||
displayName = "Conny Duck",
|
||||
note = "This is their bio",
|
||||
url = "https://$domain/@ConnyDuck",
|
||||
avatar = "https://$domain/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg"
|
||||
)
|
||||
|
||||
fun fakeStatus(
|
||||
id: String = "100",
|
||||
authorServerId: String = "100",
|
||||
inReplyToId: String? = null,
|
||||
inReplyToAccountId: String? = null,
|
||||
spoilerText: String = "",
|
||||
reblogged: Boolean = false,
|
||||
favourited: Boolean = true,
|
||||
bookmarked: Boolean = true,
|
||||
domain: String = "mastodon.example",
|
||||
reblog: Status? = null
|
||||
) = Status(
|
||||
id = id,
|
||||
url = "https://$domain/@ConnyDuck/$id",
|
||||
account = fakeAccount(
|
||||
id = authorServerId,
|
||||
domain = domain
|
||||
),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
reblog = reblog,
|
||||
content = "Test",
|
||||
createdAt = fixedDate,
|
||||
editedAt = null,
|
||||
emojis = emptyList(),
|
||||
reblogsCount = 1,
|
||||
favouritesCount = 2,
|
||||
repliesCount = 3,
|
||||
reblogged = reblogged,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = true,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.PUBLIC,
|
||||
attachments = ArrayList(),
|
||||
mentions = emptyList(),
|
||||
tags = emptyList(),
|
||||
application = Status.Application("Tusky", "https://tusky.app"),
|
||||
pinned = false,
|
||||
muted = false,
|
||||
poll = null,
|
||||
card = null,
|
||||
language = null,
|
||||
filtered = emptyList()
|
||||
)
|
||||
|
||||
fun fakeStatusViewData(
|
||||
id: String = "100",
|
||||
inReplyToId: String? = null,
|
||||
inReplyToAccountId: String? = null,
|
||||
isDetailed: Boolean = false,
|
||||
spoilerText: String = "",
|
||||
isExpanded: Boolean = false,
|
||||
isShowingContent: Boolean = false,
|
||||
isCollapsed: Boolean = !isDetailed,
|
||||
reblogged: Boolean = false,
|
||||
favourited: Boolean = true,
|
||||
bookmarked: Boolean = true
|
||||
) = StatusViewData.Concrete(
|
||||
status = fakeStatus(
|
||||
id = id,
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
spoilerText = spoilerText,
|
||||
reblogged = reblogged,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked
|
||||
),
|
||||
isExpanded = isExpanded,
|
||||
isShowingContent = isShowingContent,
|
||||
isCollapsed = isCollapsed,
|
||||
isDetailed = isDetailed
|
||||
)
|
||||
|
||||
fun fakeHomeTimelineData(
|
||||
id: String = "100",
|
||||
statusId: String = id,
|
||||
tuskyAccountId: Long = 1,
|
||||
authorServerId: String = "100",
|
||||
expanded: Boolean = false,
|
||||
domain: String = "mastodon.example",
|
||||
reblogAuthorServerId: String? = null
|
||||
): HomeTimelineData {
|
||||
val mockedStatus = fakeStatus(
|
||||
id = statusId,
|
||||
authorServerId = authorServerId,
|
||||
domain = domain
|
||||
)
|
||||
|
||||
return HomeTimelineData(
|
||||
id = id,
|
||||
status = mockedStatus.toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
expanded = expanded,
|
||||
contentShowing = false,
|
||||
contentCollapsed = true
|
||||
),
|
||||
account = mockedStatus.account.toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
),
|
||||
reblogAccount = reblogAuthorServerId?.let { reblogAuthorId ->
|
||||
fakeAccount(
|
||||
id = reblogAuthorId
|
||||
).toEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
)
|
||||
},
|
||||
repliedToAccount = null,
|
||||
loading = false
|
||||
)
|
||||
}
|
||||
|
||||
fun fakePlaceholderHomeTimelineData(
|
||||
id: String
|
||||
) = HomeTimelineData(
|
||||
id = id,
|
||||
account = null,
|
||||
status = null,
|
||||
reblogAccount = null,
|
||||
repliedToAccount = null,
|
||||
loading = false
|
||||
)
|
||||
|
||||
suspend fun AppDatabase.insert(timelineItems: List<HomeTimelineData>, tuskyAccountId: Long = 1) = withTransaction {
|
||||
timelineItems.forEach { timelineItem ->
|
||||
timelineItem.account?.let { account ->
|
||||
timelineAccountDao().insert(account)
|
||||
}
|
||||
timelineItem.reblogAccount?.let { account ->
|
||||
timelineAccountDao().insert(account)
|
||||
}
|
||||
timelineItem.status?.let { status ->
|
||||
timelineStatusDao().insert(status)
|
||||
}
|
||||
timelineDao().insertHomeTimelineItem(
|
||||
HomeTimelineEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
id = timelineItem.id,
|
||||
statusId = timelineItem.status?.serverId,
|
||||
reblogAccountId = timelineItem.reblogAccount?.serverId,
|
||||
loading = timelineItem.loading
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun AppDatabase.assertTimeline(
|
||||
expected: List<HomeTimelineData>,
|
||||
tuskyAccountId: Long = 1
|
||||
) {
|
||||
val pagingSource = timelineDao().getHomeTimeline(tuskyAccountId)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(expected.size, loadedStatuses.size)
|
||||
|
||||
for ((exp, prov) in expected.zip(loadedStatuses)) {
|
||||
assertEquals(exp.status, prov.status)
|
||||
assertEquals(exp.account, prov.account)
|
||||
assertEquals(exp.reblogAccount, prov.reblogAccount)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,13 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
||||
import com.keylesspalace.tusky.components.timeline.mockStatus
|
||||
import com.keylesspalace.tusky.components.timeline.mockStatusViewData
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||
import com.keylesspalace.tusky.components.timeline.fakeStatus
|
||||
import com.keylesspalace.tusky.components.timeline.fakeStatusViewData
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.StatusContext
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
|
|
@ -21,7 +22,7 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
|
|
@ -34,7 +35,7 @@ import org.mockito.kotlin.stub
|
|||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ViewThreadViewModelTest {
|
||||
|
||||
|
|
@ -81,8 +82,11 @@ class ViewThreadViewModelTest {
|
|||
api = mock {
|
||||
onBlocking { getFilters() } doReturn NetworkResult.success(emptyList())
|
||||
}
|
||||
val instanceInfoRepo: InstanceInfoRepository = mock {
|
||||
onBlocking { isFilterV2Supported() } doReturn false
|
||||
}
|
||||
eventHub = EventHub()
|
||||
val filterModel = FilterModel()
|
||||
val filterModel = FilterModel(instanceInfoRepo, api)
|
||||
val timelineCases = TimelineCases(api, eventHub)
|
||||
val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doReturn AccountEntity(
|
||||
|
|
@ -109,52 +113,48 @@ class ViewThreadViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `should emit status and context when both load`() {
|
||||
fun `should emit status and context when both load`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit status even if context fails to load`() {
|
||||
fun `should emit status even if context fails to load`() = runTest {
|
||||
api.stub {
|
||||
onBlocking { status(threadId) } doReturn NetworkResult.success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1"))
|
||||
onBlocking { status(threadId) } doReturn NetworkResult.success(fakeStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1"))
|
||||
onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException())
|
||||
}
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true)
|
||||
),
|
||||
detailedStatusPosition = 0,
|
||||
revealButton = RevealButtonState.NO_BUTTON
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true)
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 0,
|
||||
revealButton = RevealButtonState.NO_BUTTON
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit error when status and context fail to load`() {
|
||||
fun `should emit error when status and context fail to load`() = runTest {
|
||||
api.stub {
|
||||
onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException())
|
||||
onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException())
|
||||
|
|
@ -162,194 +162,178 @@ class ViewThreadViewModelTest {
|
|||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Error::class.java,
|
||||
viewModel.uiState.first().javaClass
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
ThreadUiState.Error::class.java,
|
||||
viewModel.uiState.first().javaClass
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit error when status fails to load`() {
|
||||
fun `should emit error when status fails to load`() = runTest {
|
||||
api.stub {
|
||||
onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException())
|
||||
onBlocking { statusContext(threadId) } doReturn NetworkResult.success(
|
||||
StatusContext(
|
||||
ancestors = listOf(mockStatus(id = "1")),
|
||||
descendants = listOf(mockStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1"))
|
||||
ancestors = listOf(fakeStatus(id = "1")),
|
||||
descendants = listOf(fakeStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Error::class.java,
|
||||
viewModel.uiState.first().javaClass
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
ThreadUiState.Error::class.java,
|
||||
viewModel.uiState.first().javaClass
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should update state when reveal button is toggled`() {
|
||||
fun `should update state when reveal button is toggled`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
viewModel.toggleRevealButton()
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test", isExpanded = true),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test", isExpanded = true)
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.HIDE
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test", isExpanded = true),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true),
|
||||
fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test", isExpanded = true)
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.HIDE
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle status changed event`() {
|
||||
fun `should handle status changed event`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
runBlocking {
|
||||
eventHub.dispatch(StatusChangedEvent(mockStatus(id = "1", spoilerText = "Test", favourited = false)))
|
||||
eventHub.dispatch(StatusChangedEvent(fakeStatus(id = "1", spoilerText = "Test", favourited = false)))
|
||||
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test", favourited = false),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test", favourited = false),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should remove status`() {
|
||||
fun `should remove status`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
viewModel.removeStatus(mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test"))
|
||||
viewModel.removeStatus(fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test"))
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change status expanded state`() {
|
||||
fun `should change status expanded state`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
viewModel.changeExpanded(
|
||||
true,
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true),
|
||||
fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change content collapsed state`() {
|
||||
fun `should change content collapsed state`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
viewModel.changeContentCollapsed(
|
||||
true,
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isCollapsed = true),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isCollapsed = true),
|
||||
fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should change content showing state`() {
|
||||
fun `should change content showing state`() = runTest {
|
||||
mockSuccessResponses()
|
||||
|
||||
viewModel.loadThread(threadId)
|
||||
|
||||
viewModel.changeContentShowing(
|
||||
true,
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "1", spoilerText = "Test"),
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isShowingContent = true),
|
||||
mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isShowingContent = true),
|
||||
fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
),
|
||||
viewModel.uiState.first()
|
||||
)
|
||||
}
|
||||
|
||||
private fun mockSuccessResponses() {
|
||||
api.stub {
|
||||
onBlocking { status(threadId) } doReturn NetworkResult.success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1", spoilerText = "Test"))
|
||||
onBlocking { status(threadId) } doReturn NetworkResult.success(fakeStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1", spoilerText = "Test"))
|
||||
onBlocking { statusContext(threadId) } doReturn NetworkResult.success(
|
||||
StatusContext(
|
||||
ancestors = listOf(mockStatus(id = "1", spoilerText = "Test")),
|
||||
descendants = listOf(mockStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test"))
|
||||
ancestors = listOf(fakeStatus(id = "1", spoilerText = "Test")),
|
||||
descendants = listOf(fakeStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
package com.keylesspalace.tusky.db
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.di.StorageModule
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MigrationsTest {
|
||||
|
||||
@get:Rule
|
||||
val migrationHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testMigrations() = runTest {
|
||||
/** the db name must match the one in [StorageModule.providesDatabase] */
|
||||
val db = migrationHelper.createDatabase("tuskyDB", 10)
|
||||
val moshi = Moshi.Builder().build()
|
||||
|
||||
val id = 1L
|
||||
val domain = "domain.site"
|
||||
val token = "token"
|
||||
val active = true
|
||||
val accountId = "accountId"
|
||||
val username = "username"
|
||||
val emoji = moshi.adapter<List<Emoji>>(Types.newParameterizedType(List::class.java, Emoji::class.java), emptySet()).toJson(
|
||||
listOf(
|
||||
Emoji(
|
||||
shortcode = "testemoji",
|
||||
url = "https://some.url",
|
||||
staticUrl = "https://some.url",
|
||||
visibleInPicker = true,
|
||||
category = null
|
||||
)
|
||||
)
|
||||
)
|
||||
val values = arrayOf(
|
||||
id, domain, token, active, accountId, username, "Display Name",
|
||||
"https://picture.url", true, true, true, true, true, true, true,
|
||||
true, "1000", "[]", emoji, 0, false,
|
||||
false, true
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
|
||||
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
|
||||
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
|
||||
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
|
||||
"`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," +
|
||||
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
|
||||
"`mediaPreviewEnabled`) " +
|
||||
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
values
|
||||
)
|
||||
|
||||
db.close()
|
||||
|
||||
// Room will run all migrations and validate the scheme afterwards
|
||||
val roomDb = StorageModule.providesDatabase(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
Converters(moshi)
|
||||
)
|
||||
|
||||
val account = roomDb.accountDao().allAccounts().first().first()
|
||||
|
||||
roomDb.close()
|
||||
|
||||
assertEquals(id, account.id)
|
||||
assertEquals(domain, account.domain)
|
||||
assertEquals(token, account.accessToken)
|
||||
assertEquals(active, account.isActive)
|
||||
assertEquals(accountId, account.accountId)
|
||||
assertEquals(username, account.username)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,490 +0,0 @@
|
|||
package com.keylesspalace.tusky.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimelineDaoTest {
|
||||
private lateinit var timelineDao: TimelineDao
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
timelineDao = db.timelineDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertGetStatus() = runBlocking {
|
||||
val setOne = makeStatus(statusId = 3)
|
||||
val setTwo = makeStatus(statusId = 20, reblog = true)
|
||||
val ignoredOne = makeStatus(statusId = 1)
|
||||
val ignoredTwo = makeStatus(accountId = 2)
|
||||
|
||||
for ((status, author, reblogger) in listOf(setOne, setTwo, ignoredOne, ignoredTwo)) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogger?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
val pagingSource = timelineDao.getStatuses(setOne.first.timelineUserId)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(2, loadedStatuses.size)
|
||||
assertStatuses(listOf(setTwo, setOne), loadedStatuses)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanup() = runBlocking {
|
||||
val statusesBeforeCleanup = listOf(
|
||||
makeStatus(statusId = 100),
|
||||
makeStatus(statusId = 10, authorServerId = "3"),
|
||||
makeStatus(statusId = 8, reblog = true, authorServerId = "10"),
|
||||
makeStatus(statusId = 5),
|
||||
makeStatus(statusId = 3, authorServerId = "4"),
|
||||
makeStatus(statusId = 2, accountId = 2, authorServerId = "5"),
|
||||
makeStatus(statusId = 1, authorServerId = "5")
|
||||
)
|
||||
|
||||
val statusesAfterCleanup = listOf(
|
||||
makeStatus(statusId = 100),
|
||||
makeStatus(statusId = 10, authorServerId = "3"),
|
||||
makeStatus(statusId = 8, reblog = true, authorServerId = "10"),
|
||||
makeStatus(statusId = 2, accountId = 2, authorServerId = "5")
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in statusesBeforeCleanup) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
timelineDao.cleanup(accountId = 1, limit = 3)
|
||||
timelineDao.cleanupAccounts(accountId = 1)
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val loadedStatuses = (timelineDao.getStatuses(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertStatuses(statusesAfterCleanup, loadedStatuses)
|
||||
|
||||
val loadedAccounts: MutableList<Pair<Long, String>> = mutableListOf()
|
||||
val accountCursor = db.query("SELECT timelineUserId, serverId FROM TimelineAccountEntity ORDER BY timelineUserId, serverId", null)
|
||||
accountCursor.moveToFirst()
|
||||
while (!accountCursor.isAfterLast) {
|
||||
val accountId: Long = accountCursor.getLong(accountCursor.getColumnIndex("timelineUserId"))
|
||||
val serverId: String = accountCursor.getString(accountCursor.getColumnIndex("serverId"))
|
||||
loadedAccounts.add(accountId to serverId)
|
||||
accountCursor.moveToNext()
|
||||
}
|
||||
|
||||
val expectedAccounts = listOf(
|
||||
1L to "10",
|
||||
1L to "20",
|
||||
1L to "3",
|
||||
1L to "R10",
|
||||
2L to "5"
|
||||
)
|
||||
|
||||
assertEquals(expectedAccounts, loadedAccounts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overwriteDeletedStatus() = runBlocking {
|
||||
val oldStatuses = listOf(
|
||||
makeStatus(statusId = 3),
|
||||
makeStatus(statusId = 2),
|
||||
makeStatus(statusId = 1)
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in oldStatuses) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
// status 2 gets deleted, newly loaded status contain only 1 + 3
|
||||
val newStatuses = listOf(
|
||||
makeStatus(statusId = 3),
|
||||
makeStatus(statusId = 1)
|
||||
)
|
||||
|
||||
val deletedCount = timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId)
|
||||
assertEquals(3, deletedCount)
|
||||
|
||||
for ((status, author, reblogAuthor) in newStatuses) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
// make sure status 2 is no longer in db
|
||||
|
||||
val pagingSource = timelineDao.getStatuses(1)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertStatuses(newStatuses, loadedStatuses)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteRange() = runBlocking {
|
||||
val statuses = listOf(
|
||||
makeStatus(statusId = 100),
|
||||
makeStatus(statusId = 50),
|
||||
makeStatus(statusId = 15),
|
||||
makeStatus(statusId = 14),
|
||||
makeStatus(statusId = 13),
|
||||
makeStatus(statusId = 13, accountId = 2),
|
||||
makeStatus(statusId = 12),
|
||||
makeStatus(statusId = 11),
|
||||
makeStatus(statusId = 9)
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in statuses) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
assertEquals(3, timelineDao.deleteRange(1, "12", "14"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "80", "80"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "60", "80"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "5", "8"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "101", "1000"))
|
||||
assertEquals(1, timelineDao.deleteRange(1, "50", "50"))
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val statusesAccount1 = (timelineDao.getStatuses(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
val statusesAccount2 = (timelineDao.getStatuses(2).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
val remainingStatusesAccount1 = listOf(
|
||||
makeStatus(statusId = 100),
|
||||
makeStatus(statusId = 15),
|
||||
makeStatus(statusId = 11),
|
||||
makeStatus(statusId = 9)
|
||||
)
|
||||
|
||||
val remainingStatusesAccount2 = listOf(
|
||||
makeStatus(statusId = 13, accountId = 2)
|
||||
)
|
||||
|
||||
assertStatuses(remainingStatusesAccount1, statusesAccount1)
|
||||
assertStatuses(remainingStatusesAccount2, statusesAccount2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllForInstance() = runBlocking {
|
||||
val statusWithRedDomain1 = makeStatus(
|
||||
statusId = 15,
|
||||
accountId = 1,
|
||||
domain = "mastodon.red",
|
||||
authorServerId = "1"
|
||||
)
|
||||
val statusWithRedDomain2 = makeStatus(
|
||||
statusId = 14,
|
||||
accountId = 1,
|
||||
domain = "mastodon.red",
|
||||
authorServerId = "2"
|
||||
)
|
||||
val statusWithRedDomainOtherAccount = makeStatus(
|
||||
statusId = 12,
|
||||
accountId = 2,
|
||||
domain = "mastodon.red",
|
||||
authorServerId = "2"
|
||||
)
|
||||
val statusWithBlueDomain = makeStatus(
|
||||
statusId = 10,
|
||||
accountId = 1,
|
||||
domain = "mastodon.blue",
|
||||
authorServerId = "4"
|
||||
)
|
||||
val statusWithBlueDomainOtherAccount = makeStatus(
|
||||
statusId = 10,
|
||||
accountId = 2,
|
||||
domain = "mastodon.blue",
|
||||
authorServerId = "5"
|
||||
)
|
||||
val statusWithGreenDomain = makeStatus(
|
||||
statusId = 8,
|
||||
accountId = 1,
|
||||
domain = "mastodon.green",
|
||||
authorServerId = "6"
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in listOf(statusWithRedDomain1, statusWithRedDomain2, statusWithRedDomainOtherAccount, statusWithBlueDomain, statusWithBlueDomainOtherAccount, statusWithGreenDomain)) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
timelineDao.deleteAllFromInstance(1, "mastodon.red")
|
||||
timelineDao.deleteAllFromInstance(1, "mastodon.blu") // shouldn't delete anything
|
||||
timelineDao.deleteAllFromInstance(1, "greenmastodon.green") // shouldn't delete anything
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val statusesAccount1 = (timelineDao.getStatuses(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
val statusesAccount2 = (timelineDao.getStatuses(2).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertStatuses(listOf(statusWithBlueDomain, statusWithGreenDomain), statusesAccount1)
|
||||
assertStatuses(listOf(statusWithRedDomainOtherAccount, statusWithBlueDomainOtherAccount), statusesAccount2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return null as topId when db is empty`() = runBlocking {
|
||||
assertNull(timelineDao.getTopId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct topId`() = runBlocking {
|
||||
val statusData = listOf(
|
||||
makeStatus(
|
||||
statusId = 4,
|
||||
accountId = 1,
|
||||
domain = "mastodon.test",
|
||||
authorServerId = "1"
|
||||
),
|
||||
makeStatus(
|
||||
statusId = 33,
|
||||
accountId = 1,
|
||||
domain = "mastodon.test",
|
||||
authorServerId = "2"
|
||||
),
|
||||
makeStatus(
|
||||
statusId = 22,
|
||||
accountId = 1,
|
||||
domain = "mastodon.test",
|
||||
authorServerId = "2"
|
||||
)
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in statusData) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
assertEquals("33", timelineDao.getTopId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct placeholderId after other ids`() = runBlocking {
|
||||
val statusData = listOf(
|
||||
makeStatus(statusId = 1000),
|
||||
makePlaceholder(id = 99),
|
||||
makeStatus(statusId = 97),
|
||||
makeStatus(statusId = 95),
|
||||
makePlaceholder(id = 94),
|
||||
makeStatus(statusId = 90)
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in statusData) {
|
||||
author?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
assertEquals("99", timelineDao.getNextPlaceholderIdAfter(1, "1000"))
|
||||
assertEquals("94", timelineDao.getNextPlaceholderIdAfter(1, "99"))
|
||||
assertNull(timelineDao.getNextPlaceholderIdAfter(1, "90"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct top placeholderId`() = runBlocking {
|
||||
val statusData = listOf(
|
||||
makeStatus(statusId = 1000),
|
||||
makePlaceholder(id = 99),
|
||||
makeStatus(statusId = 97),
|
||||
makePlaceholder(id = 96),
|
||||
makeStatus(statusId = 90),
|
||||
makePlaceholder(id = 80),
|
||||
makeStatus(statusId = 77)
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in statusData) {
|
||||
author?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
reblogAuthor?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
assertEquals("99", timelineDao.getTopPlaceholderId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `preview card survives roundtrip`() = runBlocking {
|
||||
val setOne = makeStatus(statusId = 3, cardUrl = "https://foo.bar")
|
||||
|
||||
for ((status, author, reblogger) in listOf(setOne)) {
|
||||
timelineDao.insertAccount(author)
|
||||
reblogger?.let {
|
||||
timelineDao.insertAccount(it)
|
||||
}
|
||||
timelineDao.insertStatus(status)
|
||||
}
|
||||
|
||||
val pagingSource = timelineDao.getStatuses(setOne.first.timelineUserId)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(1, loadedStatuses.size)
|
||||
assertStatuses(listOf(setOne), loadedStatuses)
|
||||
}
|
||||
|
||||
private fun makeStatus(
|
||||
accountId: Long = 1,
|
||||
statusId: Long = 10,
|
||||
reblog: Boolean = false,
|
||||
createdAt: Long = statusId,
|
||||
authorServerId: String = "20",
|
||||
domain: String = "mastodon.example",
|
||||
cardUrl: String? = null
|
||||
): Triple<TimelineStatusEntity, TimelineAccountEntity, TimelineAccountEntity?> {
|
||||
val author = TimelineAccountEntity(
|
||||
serverId = authorServerId,
|
||||
timelineUserId = accountId,
|
||||
localUsername = "localUsername@$domain",
|
||||
username = "username@$domain",
|
||||
displayName = "displayName",
|
||||
url = "blah",
|
||||
avatar = "avatar",
|
||||
emojis = "[\"tusky\": \"http://tusky.cool/emoji.jpg\"]",
|
||||
bot = false
|
||||
)
|
||||
|
||||
val reblogAuthor = if (reblog) {
|
||||
TimelineAccountEntity(
|
||||
serverId = "R$authorServerId",
|
||||
timelineUserId = accountId,
|
||||
localUsername = "RlocalUsername",
|
||||
username = "Rusername",
|
||||
displayName = "RdisplayName",
|
||||
url = "Rblah",
|
||||
avatar = "Ravatar",
|
||||
emojis = "[]",
|
||||
bot = false
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val card = when (cardUrl) {
|
||||
null -> null
|
||||
else -> "{ url: \"$cardUrl\" }"
|
||||
}
|
||||
val even = accountId % 2 == 0L
|
||||
val status = TimelineStatusEntity(
|
||||
serverId = statusId.toString(),
|
||||
url = "https://$domain/whatever/$statusId",
|
||||
timelineUserId = accountId,
|
||||
authorServerId = authorServerId,
|
||||
inReplyToId = "inReplyToId$statusId",
|
||||
inReplyToAccountId = "inReplyToAccountId$statusId",
|
||||
content = "Content!$statusId",
|
||||
createdAt = createdAt,
|
||||
editedAt = null,
|
||||
emojis = "emojis$statusId",
|
||||
reblogsCount = 1 * statusId.toInt(),
|
||||
favouritesCount = 2 * statusId.toInt(),
|
||||
repliesCount = 3 * statusId.toInt(),
|
||||
reblogged = even,
|
||||
favourited = !even,
|
||||
bookmarked = false,
|
||||
sensitive = even,
|
||||
spoilerText = "spoiler$statusId",
|
||||
visibility = Status.Visibility.PRIVATE,
|
||||
attachments = "attachments$accountId",
|
||||
mentions = "mentions$accountId",
|
||||
tags = "tags$accountId",
|
||||
application = "application$accountId",
|
||||
reblogServerId = if (reblog) (statusId * 100).toString() else null,
|
||||
reblogAccountId = reblogAuthor?.serverId,
|
||||
poll = null,
|
||||
muted = false,
|
||||
expanded = false,
|
||||
contentCollapsed = false,
|
||||
contentShowing = true,
|
||||
pinned = false,
|
||||
card = card,
|
||||
language = null,
|
||||
filtered = null
|
||||
)
|
||||
return Triple(status, author, reblogAuthor)
|
||||
}
|
||||
|
||||
private fun makePlaceholder(
|
||||
accountId: Long = 1,
|
||||
id: Long
|
||||
): Triple<TimelineStatusEntity, TimelineAccountEntity?, TimelineAccountEntity?> {
|
||||
val placeholder = Placeholder(id.toString(), false).toEntity(accountId)
|
||||
return Triple(placeholder, null, null)
|
||||
}
|
||||
|
||||
private fun assertStatuses(
|
||||
expected: List<Triple<TimelineStatusEntity, TimelineAccountEntity, TimelineAccountEntity?>>,
|
||||
provided: List<TimelineStatusWithAccount>
|
||||
) {
|
||||
for ((exp, prov) in expected.zip(provided)) {
|
||||
val (status, author, reblogger) = exp
|
||||
assertEquals(status, prov.status)
|
||||
assertEquals(author, prov.account)
|
||||
assertEquals(reblogger, prov.reblogAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
package com.keylesspalace.tusky.db.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.components.notifications.fakeNotification
|
||||
import com.keylesspalace.tusky.components.notifications.fakeReport
|
||||
import com.keylesspalace.tusky.components.notifications.insert
|
||||
import com.keylesspalace.tusky.components.timeline.fakeAccount
|
||||
import com.keylesspalace.tusky.components.timeline.fakeHomeTimelineData
|
||||
import com.keylesspalace.tusky.components.timeline.fakeStatus
|
||||
import com.keylesspalace.tusky.components.timeline.insert
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.db.DatabaseCleaner
|
||||
import com.keylesspalace.tusky.db.entity.HomeTimelineEntity
|
||||
import com.keylesspalace.tusky.db.entity.NotificationEntity
|
||||
import com.keylesspalace.tusky.db.entity.NotificationReportEntity
|
||||
import com.keylesspalace.tusky.db.entity.TimelineAccountEntity
|
||||
import com.keylesspalace.tusky.db.entity.TimelineStatusEntity
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import kotlin.reflect.KClass
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DatabaseCleanerTest {
|
||||
private lateinit var timelineDao: TimelineDao
|
||||
private lateinit var dbCleaner: DatabaseCleaner
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
timelineDao = db.timelineDao()
|
||||
dbCleaner = DatabaseCleaner(db)
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanupOldData() = runTest {
|
||||
fillDatabase()
|
||||
|
||||
dbCleaner.cleanupOldData(tuskyAccountId = 1, timelineLimit = 3, notificationLimit = 3)
|
||||
|
||||
// all but 3 timeline items and notifications and all references items should be gone for Tusky account 1
|
||||
// items of Tusky account 2 should be untouched
|
||||
expect(
|
||||
hometimelineItems = listOf(
|
||||
1L to "10",
|
||||
1L to "100",
|
||||
1L to "8",
|
||||
2L to "2"
|
||||
),
|
||||
statuses = listOf(
|
||||
1L to "10",
|
||||
1L to "100",
|
||||
1L to "8",
|
||||
1L to "n3",
|
||||
1L to "n4",
|
||||
1L to "n5",
|
||||
2L to "2",
|
||||
2L to "n1",
|
||||
2L to "n2",
|
||||
2L to "n3",
|
||||
2L to "n4"
|
||||
),
|
||||
notifications = listOf(
|
||||
1L to "3",
|
||||
1L to "4",
|
||||
1L to "5",
|
||||
2L to "1",
|
||||
2L to "2",
|
||||
2L to "3",
|
||||
2L to "4",
|
||||
),
|
||||
accounts = listOf(
|
||||
1L to "10",
|
||||
1L to "100",
|
||||
1L to "3",
|
||||
1L to "R10",
|
||||
1L to "n3",
|
||||
1L to "n4",
|
||||
1L to "n5",
|
||||
1L to "r2",
|
||||
2L to "100",
|
||||
2L to "5",
|
||||
2L to "n1",
|
||||
2L to "n2",
|
||||
2L to "n3",
|
||||
2L to "n4",
|
||||
2L to "r1"
|
||||
),
|
||||
reports = listOf(
|
||||
1L to "2",
|
||||
2L to "1"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanupEverything() = runTest {
|
||||
fillDatabase()
|
||||
|
||||
dbCleaner.cleanupEverything(tuskyAccountId = 1)
|
||||
|
||||
// everything from Tusky account 1 should be gone
|
||||
// items of Tusky account 2 should be untouched
|
||||
expect(
|
||||
hometimelineItems = listOf(
|
||||
2L to "2"
|
||||
),
|
||||
statuses = listOf(
|
||||
2L to "2",
|
||||
2L to "n1",
|
||||
2L to "n2",
|
||||
2L to "n3",
|
||||
2L to "n4"
|
||||
),
|
||||
notifications = listOf(
|
||||
2L to "1",
|
||||
2L to "2",
|
||||
2L to "3",
|
||||
2L to "4",
|
||||
),
|
||||
accounts = listOf(
|
||||
2L to "100",
|
||||
2L to "5",
|
||||
2L to "n1",
|
||||
2L to "n2",
|
||||
2L to "n3",
|
||||
2L to "n4",
|
||||
2L to "r1"
|
||||
),
|
||||
reports = listOf(
|
||||
2L to "1"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fillDatabase() {
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeHomeTimelineData(id = "100", authorServerId = "100"),
|
||||
fakeHomeTimelineData(id = "10", authorServerId = "3"),
|
||||
fakeHomeTimelineData(id = "8", reblogAuthorServerId = "R10", authorServerId = "10"),
|
||||
fakeHomeTimelineData(id = "5", authorServerId = "100"),
|
||||
fakeHomeTimelineData(id = "3", authorServerId = "4"),
|
||||
fakeHomeTimelineData(id = "1", authorServerId = "5")
|
||||
),
|
||||
tuskyAccountId = 1
|
||||
)
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeHomeTimelineData(id = "2", tuskyAccountId = 2, authorServerId = "5")
|
||||
),
|
||||
tuskyAccountId = 2
|
||||
)
|
||||
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeNotification(id = "1", account = fakeAccount(id = "n1"), status = fakeStatus(id = "n1")),
|
||||
fakeNotification(id = "2", account = fakeAccount(id = "n2"), status = fakeStatus(id = "n2"), report = fakeReport(targetAccount = fakeAccount(id = "r1"))),
|
||||
fakeNotification(id = "3", account = fakeAccount(id = "n3"), status = fakeStatus(id = "n3")),
|
||||
fakeNotification(id = "4", account = fakeAccount(id = "n4"), status = fakeStatus(id = "n4"), report = fakeReport(id = "2", targetAccount = fakeAccount(id = "r2"))),
|
||||
fakeNotification(id = "5", account = fakeAccount(id = "n5"), status = fakeStatus(id = "n5")),
|
||||
),
|
||||
tuskyAccountId = 1
|
||||
)
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeNotification(id = "1", account = fakeAccount(id = "n1"), status = fakeStatus(id = "n1")),
|
||||
fakeNotification(id = "2", account = fakeAccount(id = "n2"), status = fakeStatus(id = "n2")),
|
||||
fakeNotification(id = "3", account = fakeAccount(id = "n3"), status = fakeStatus(id = "n3")),
|
||||
fakeNotification(id = "4", account = fakeAccount(id = "n4"), status = fakeStatus(id = "n4"), report = fakeReport(targetAccount = fakeAccount(id = "r1")))
|
||||
),
|
||||
tuskyAccountId = 2
|
||||
)
|
||||
}
|
||||
|
||||
private fun expect(
|
||||
hometimelineItems: List<Pair<Long, String>>,
|
||||
statuses: List<Pair<Long, String>>,
|
||||
notifications: List<Pair<Long, String>>,
|
||||
accounts: List<Pair<Long, String>>,
|
||||
reports: List<Pair<Long, String>>,
|
||||
) {
|
||||
expect(HomeTimelineEntity::class, "id", hometimelineItems)
|
||||
expect(TimelineStatusEntity::class, "serverId", statuses)
|
||||
expect(NotificationEntity::class, "id", notifications)
|
||||
expect(TimelineAccountEntity::class, "serverId", accounts)
|
||||
expect(NotificationReportEntity::class, "serverId", reports)
|
||||
}
|
||||
|
||||
private fun expect(
|
||||
entity: KClass<*>,
|
||||
idName: String,
|
||||
expectedItems: List<Pair<Long, String>>
|
||||
) {
|
||||
val loadedItems: MutableList<Pair<Long, String>> = mutableListOf()
|
||||
val cursor = db.query("SELECT tuskyAccountId, $idName FROM ${entity.simpleName} ORDER BY tuskyAccountId, $idName", null)
|
||||
cursor.moveToFirst()
|
||||
while (!cursor.isAfterLast) {
|
||||
val tuskyAccountId: Long = cursor.getLong(cursor.getColumnIndex("tuskyAccountId"))
|
||||
val id: String = cursor.getString(cursor.getColumnIndex(idName))
|
||||
loadedItems.add(tuskyAccountId to id)
|
||||
cursor.moveToNext()
|
||||
}
|
||||
cursor.close()
|
||||
|
||||
assertEquals(expectedItems, loadedItems)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
package com.keylesspalace.tusky.db.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.components.notifications.fakeNotification
|
||||
import com.keylesspalace.tusky.components.notifications.fakeReport
|
||||
import com.keylesspalace.tusky.components.notifications.insert
|
||||
import com.keylesspalace.tusky.components.notifications.toNotificationDataEntity
|
||||
import com.keylesspalace.tusky.components.notifications.toNotificationEntity
|
||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||
import com.keylesspalace.tusky.components.timeline.fakeAccount
|
||||
import com.keylesspalace.tusky.components.timeline.fakeStatus
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NotificationsDaoTest {
|
||||
private lateinit var notificationsDao: NotificationsDao
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
notificationsDao = db.notificationsDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertAndGetNotification() = runTest {
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeNotification(id = "1"),
|
||||
fakeNotification(id = "2"),
|
||||
fakeNotification(id = "3"),
|
||||
),
|
||||
tuskyAccountId = 1
|
||||
)
|
||||
db.insert(
|
||||
listOf(fakeNotification(id = "3")),
|
||||
tuskyAccountId = 2
|
||||
)
|
||||
|
||||
val pagingSource = notificationsDao.getNotifications(tuskyAccountId = 1)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
fakeNotification(id = "3").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "2").toNotificationDataEntity(1)
|
||||
),
|
||||
loadedStatuses
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteRange() = runTest {
|
||||
val notifications = listOf(
|
||||
fakeNotification(id = "100"),
|
||||
fakeNotification(id = "50"),
|
||||
fakeNotification(id = "15"),
|
||||
fakeNotification(id = "14"),
|
||||
fakeNotification(id = "13"),
|
||||
fakeNotification(id = "12"),
|
||||
fakeNotification(id = "11"),
|
||||
fakeNotification(id = "9")
|
||||
)
|
||||
|
||||
db.insert(notifications, 1)
|
||||
db.insert(listOf(fakeNotification(id = "13")), 2)
|
||||
|
||||
assertEquals(3, notificationsDao.deleteRange(1, "12", "14"))
|
||||
assertEquals(0, notificationsDao.deleteRange(1, "80", "80"))
|
||||
assertEquals(0, notificationsDao.deleteRange(1, "60", "80"))
|
||||
assertEquals(0, notificationsDao.deleteRange(1, "5", "8"))
|
||||
assertEquals(0, notificationsDao.deleteRange(1, "101", "1000"))
|
||||
assertEquals(1, notificationsDao.deleteRange(1, "50", "50"))
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val notificationsAccount1 = (notificationsDao.getNotifications(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
val notificationsAccount2 = (notificationsDao.getNotifications(2).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
val remainingNotificationsAccount1 = listOf(
|
||||
fakeNotification(id = "100").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "15").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "11").toNotificationDataEntity(1),
|
||||
fakeNotification(id = "9").toNotificationDataEntity(1)
|
||||
)
|
||||
|
||||
val remainingNotificationsAccount2 = listOf(
|
||||
fakeNotification(id = "13").toNotificationDataEntity(2)
|
||||
)
|
||||
|
||||
assertEquals(remainingNotificationsAccount1, notificationsAccount1)
|
||||
assertEquals(remainingNotificationsAccount2, notificationsAccount2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllForInstance() = runTest {
|
||||
val redAccount = fakeNotification(id = "500", account = fakeAccount(id = "500", domain = "mastodon.red"))
|
||||
val blueAccount = fakeNotification(id = "501", account = fakeAccount(id = "501", domain = "mastodon.blue"))
|
||||
val redStatus = fakeNotification(id = "502", account = fakeAccount(id = "502", domain = "mastodon.example"), status = fakeStatus(id = "502", domain = "mastodon.red", authorServerId = "502a"))
|
||||
val blueStatus = fakeNotification(id = "503", account = fakeAccount(id = "503", domain = "mastodon.example"), status = fakeStatus(id = "503", domain = "mastodon.blue", authorServerId = "503a"))
|
||||
|
||||
val redStatus2 = fakeNotification(id = "600", account = fakeAccount(id = "600", domain = "mastodon.red"))
|
||||
|
||||
db.insert(listOf(redAccount, blueAccount, redStatus, blueStatus), 1)
|
||||
db.insert(listOf(redStatus2), 2)
|
||||
|
||||
notificationsDao.deleteAllFromInstance(1, "mastodon.red")
|
||||
notificationsDao.deleteAllFromInstance(1, "mastodon.blu") // shouldn't delete anything
|
||||
notificationsDao.deleteAllFromInstance(1, "mastodon.green") // shouldn't delete anything
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val notificationsAccount1 = (notificationsDao.getNotifications(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
val notificationsAccount2 = (notificationsDao.getNotifications(2).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
blueStatus.toNotificationDataEntity(1),
|
||||
blueAccount.toNotificationDataEntity(1)
|
||||
),
|
||||
notificationsAccount1
|
||||
)
|
||||
assertEquals(listOf(redStatus2.toNotificationDataEntity(2)), notificationsAccount2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return null as topId when db is empty`() = runTest {
|
||||
assertNull(notificationsDao.getTopId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct topId`() = runTest {
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeNotification(id = "100"),
|
||||
fakeNotification(id = "3"),
|
||||
fakeNotification(id = "33"),
|
||||
fakeNotification(id = "8"),
|
||||
),
|
||||
tuskyAccountId = 1
|
||||
)
|
||||
db.insert(
|
||||
listOf(
|
||||
fakeNotification(id = "200"),
|
||||
fakeNotification(id = "300"),
|
||||
fakeNotification(id = "1000"),
|
||||
),
|
||||
tuskyAccountId = 2
|
||||
)
|
||||
|
||||
assertEquals("100", notificationsDao.getTopId(1))
|
||||
assertEquals("1000", notificationsDao.getTopId(2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct top placeholderId`() = runTest {
|
||||
val notifications = listOf(
|
||||
fakeNotification(id = "1000"),
|
||||
fakeNotification(id = "97"),
|
||||
fakeNotification(id = "90"),
|
||||
fakeNotification(id = "77")
|
||||
)
|
||||
db.insert(notifications)
|
||||
|
||||
notificationsDao.insertNotification(Placeholder(id = "99", loading = false).toNotificationEntity(1))
|
||||
notificationsDao.insertNotification(Placeholder(id = "96", loading = false).toNotificationEntity(1))
|
||||
notificationsDao.insertNotification(Placeholder(id = "80", loading = false).toNotificationEntity(1))
|
||||
|
||||
assertEquals("99", notificationsDao.getTopPlaceholderId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should correctly delete all by user`() = runTest {
|
||||
val notificationsAccount1 = listOf(
|
||||
// will be removed because it is a like by account 1
|
||||
fakeNotification(id = "1", account = fakeAccount(id = "1"), status = fakeStatus(id = "1", authorServerId = "100")),
|
||||
// will be removed because it references a status by account 1
|
||||
fakeNotification(id = "2", account = fakeAccount(id = "2"), status = fakeStatus(id = "2", authorServerId = "1")),
|
||||
// will not be removed because they are admin notifications
|
||||
fakeNotification(type = Notification.Type.Report, id = "3", account = fakeAccount(id = "3"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "1"))),
|
||||
fakeNotification(type = Notification.Type.SignUp, id = "4", account = fakeAccount(id = "1"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "4"))),
|
||||
// will not be removed because it does not reference account 1
|
||||
fakeNotification(id = "5", account = fakeAccount(id = "5"), status = fakeStatus(id = "5", authorServerId = "100")),
|
||||
fakeNotification(type = Notification.Type.Follow, id = "6", account = fakeAccount(id = "1"), status = null)
|
||||
)
|
||||
|
||||
db.insert(notificationsAccount1, tuskyAccountId = 1)
|
||||
db.insert(listOf(fakeNotification(id = "2000")), tuskyAccountId = 2)
|
||||
|
||||
notificationsDao.removeAllByUser(1, "1")
|
||||
|
||||
val loadedNotifications: MutableList<String> = mutableListOf()
|
||||
val cursor = db.query("SELECT id FROM NotificationEntity ORDER BY id ASC", null)
|
||||
cursor.moveToFirst()
|
||||
while (!cursor.isAfterLast) {
|
||||
val id: String = cursor.getString(cursor.getColumnIndex("id"))
|
||||
loadedNotifications.add(id)
|
||||
cursor.moveToNext()
|
||||
}
|
||||
cursor.close()
|
||||
|
||||
val expectedNotifications = listOf("2000", "3", "4", "5")
|
||||
|
||||
assertEquals(expectedNotifications, loadedNotifications)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
package com.keylesspalace.tusky.db.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.components.timeline.fakeHomeTimelineData
|
||||
import com.keylesspalace.tusky.components.timeline.fakePlaceholderHomeTimelineData
|
||||
import com.keylesspalace.tusky.components.timeline.insert
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.di.NetworkModule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimelineDaoTest {
|
||||
private lateinit var timelineDao: TimelineDao
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private val moshi = NetworkModule.providesMoshi()
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(moshi))
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
timelineDao = db.timelineDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertGetStatus() = runTest {
|
||||
val setOne = fakeHomeTimelineData(id = "3")
|
||||
val setTwo = fakeHomeTimelineData(id = "20", reblogAuthorServerId = "R1")
|
||||
val ignoredOne = fakeHomeTimelineData(id = "1")
|
||||
val ignoredTwo = fakeHomeTimelineData(id = "2", tuskyAccountId = 2)
|
||||
|
||||
db.insert(
|
||||
listOf(setOne, setTwo, ignoredOne),
|
||||
tuskyAccountId = 1
|
||||
)
|
||||
db.insert(
|
||||
listOf(ignoredTwo),
|
||||
tuskyAccountId = 2
|
||||
)
|
||||
|
||||
val pagingSource = timelineDao.getHomeTimeline(1)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(2, loadedStatuses.size)
|
||||
assertEquals(listOf(setTwo, setOne), loadedStatuses)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overwriteDeletedStatus() = runTest {
|
||||
val oldStatuses = listOf(
|
||||
fakeHomeTimelineData(id = "3"),
|
||||
fakeHomeTimelineData(id = "2"),
|
||||
fakeHomeTimelineData(id = "1")
|
||||
)
|
||||
|
||||
db.insert(oldStatuses, 1)
|
||||
|
||||
// status 2 gets deleted, newly loaded status contain only 1 + 3
|
||||
val newStatuses = listOf(
|
||||
fakeHomeTimelineData(id = "3"),
|
||||
fakeHomeTimelineData(id = "1")
|
||||
)
|
||||
|
||||
val deletedCount = timelineDao.deleteRange(1, newStatuses.last().id, newStatuses.first().id)
|
||||
assertEquals(3, deletedCount)
|
||||
|
||||
db.insert(newStatuses, 1)
|
||||
|
||||
// make sure status 2 is no longer in db
|
||||
val pagingSource = timelineDao.getHomeTimeline(1)
|
||||
|
||||
val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false))
|
||||
|
||||
val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(newStatuses, loadedStatuses)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteRange() = runTest {
|
||||
val statuses = listOf(
|
||||
fakeHomeTimelineData(id = "100"),
|
||||
fakeHomeTimelineData(id = "50"),
|
||||
fakeHomeTimelineData(id = "15"),
|
||||
fakeHomeTimelineData(id = "14"),
|
||||
fakeHomeTimelineData(id = "13"),
|
||||
fakeHomeTimelineData(id = "13", tuskyAccountId = 2),
|
||||
fakeHomeTimelineData(id = "12"),
|
||||
fakeHomeTimelineData(id = "11"),
|
||||
fakeHomeTimelineData(id = "9")
|
||||
)
|
||||
|
||||
db.insert(statuses - statuses[5], 1)
|
||||
db.insert(listOf(statuses[5]), 2)
|
||||
|
||||
assertEquals(3, timelineDao.deleteRange(1, "12", "14"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "80", "80"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "60", "80"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "5", "8"))
|
||||
assertEquals(0, timelineDao.deleteRange(1, "101", "1000"))
|
||||
assertEquals(1, timelineDao.deleteRange(1, "50", "50"))
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val statusesAccount1 = (timelineDao.getHomeTimeline(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
val statusesAccount2 = (timelineDao.getHomeTimeline(2).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
val remainingStatusesAccount1 = listOf(
|
||||
fakeHomeTimelineData(id = "100"),
|
||||
fakeHomeTimelineData(id = "15"),
|
||||
fakeHomeTimelineData(id = "11"),
|
||||
fakeHomeTimelineData(id = "9")
|
||||
)
|
||||
|
||||
val remainingStatusesAccount2 = listOf(
|
||||
fakeHomeTimelineData(id = "13", tuskyAccountId = 2)
|
||||
)
|
||||
|
||||
assertEquals(remainingStatusesAccount1, statusesAccount1)
|
||||
assertEquals(remainingStatusesAccount2, statusesAccount2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllForInstance() = runTest {
|
||||
val statusWithRedDomain1 = fakeHomeTimelineData(
|
||||
id = "15",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.red",
|
||||
authorServerId = "1"
|
||||
)
|
||||
val statusWithRedDomain2 = fakeHomeTimelineData(
|
||||
id = "14",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.red",
|
||||
authorServerId = "2"
|
||||
)
|
||||
val statusWithRedDomainOtherAccount = fakeHomeTimelineData(
|
||||
id = "12",
|
||||
tuskyAccountId = 2,
|
||||
domain = "mastodon.red",
|
||||
authorServerId = "2"
|
||||
)
|
||||
val statusWithBlueDomain = fakeHomeTimelineData(
|
||||
id = "10",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.blue",
|
||||
authorServerId = "4"
|
||||
)
|
||||
val statusWithBlueDomainOtherAccount = fakeHomeTimelineData(
|
||||
id = "10",
|
||||
tuskyAccountId = 2,
|
||||
domain = "mastodon.blue",
|
||||
authorServerId = "5"
|
||||
)
|
||||
val statusWithGreenDomain = fakeHomeTimelineData(
|
||||
id = "8",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.green",
|
||||
authorServerId = "6"
|
||||
)
|
||||
|
||||
db.insert(listOf(statusWithRedDomain1, statusWithRedDomain2, statusWithBlueDomain, statusWithGreenDomain), 1)
|
||||
db.insert(listOf(statusWithRedDomainOtherAccount, statusWithBlueDomainOtherAccount), 2)
|
||||
|
||||
timelineDao.deleteAllFromInstance(1, "mastodon.red")
|
||||
timelineDao.deleteAllFromInstance(1, "mastodon.blu") // shouldn't delete anything
|
||||
timelineDao.deleteAllFromInstance(1, "greenmastodon.green") // shouldn't delete anything
|
||||
|
||||
val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false)
|
||||
|
||||
val statusesAccount1 = (timelineDao.getHomeTimeline(1).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
val statusesAccount2 = (timelineDao.getHomeTimeline(2).load(loadParams) as PagingSource.LoadResult.Page).data
|
||||
|
||||
assertEquals(listOf(statusWithBlueDomain, statusWithGreenDomain), statusesAccount1)
|
||||
assertEquals(listOf(statusWithRedDomainOtherAccount, statusWithBlueDomainOtherAccount), statusesAccount2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return null as topId when db is empty`() = runTest {
|
||||
assertNull(timelineDao.getTopId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct topId`() = runTest {
|
||||
val statusData = listOf(
|
||||
fakeHomeTimelineData(
|
||||
id = "4",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.test",
|
||||
authorServerId = "1"
|
||||
),
|
||||
fakeHomeTimelineData(
|
||||
id = "33",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.test",
|
||||
authorServerId = "2"
|
||||
),
|
||||
fakeHomeTimelineData(
|
||||
id = "22",
|
||||
tuskyAccountId = 1,
|
||||
domain = "mastodon.test",
|
||||
authorServerId = "2"
|
||||
)
|
||||
)
|
||||
|
||||
db.insert(statusData, 1)
|
||||
|
||||
assertEquals("33", timelineDao.getTopId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct top placeholderId`() = runTest {
|
||||
val statusData = listOf(
|
||||
fakeHomeTimelineData(id = "1000"),
|
||||
fakePlaceholderHomeTimelineData(id = "99"),
|
||||
fakeHomeTimelineData(id = "97"),
|
||||
fakePlaceholderHomeTimelineData(id = "96"),
|
||||
fakeHomeTimelineData(id = "90"),
|
||||
fakePlaceholderHomeTimelineData(id = "80"),
|
||||
fakeHomeTimelineData(id = "77")
|
||||
)
|
||||
|
||||
db.insert(statusData)
|
||||
|
||||
assertEquals("99", timelineDao.getTopPlaceholderId(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should correctly delete all by user`() = runTest {
|
||||
val statusData = listOf(
|
||||
// will be deleted because it is a direct post
|
||||
fakeHomeTimelineData(id = "0", tuskyAccountId = 1, authorServerId = "1"),
|
||||
// different Tusky Account
|
||||
fakeHomeTimelineData(id = "1", tuskyAccountId = 2, authorServerId = "1"),
|
||||
// different author
|
||||
fakeHomeTimelineData(id = "2", tuskyAccountId = 1, authorServerId = "2"),
|
||||
// different author and reblogger
|
||||
fakeHomeTimelineData(id = "3", tuskyAccountId = 1, authorServerId = "2", statusId = "100", reblogAuthorServerId = "3"),
|
||||
// will be deleted because it is a reblog
|
||||
fakeHomeTimelineData(id = "4", tuskyAccountId = 1, authorServerId = "2", statusId = "101", reblogAuthorServerId = "1"),
|
||||
// not a status
|
||||
fakePlaceholderHomeTimelineData(id = "5"),
|
||||
// will be deleted because it is a self reblog
|
||||
fakeHomeTimelineData(id = "6", tuskyAccountId = 1, authorServerId = "1", statusId = "102", reblogAuthorServerId = "1"),
|
||||
// will be deleted because it direct post reblogged by another user
|
||||
fakeHomeTimelineData(id = "7", tuskyAccountId = 1, authorServerId = "1", statusId = "103", reblogAuthorServerId = "3"),
|
||||
// different Tusky Account
|
||||
fakeHomeTimelineData(id = "8", tuskyAccountId = 2, authorServerId = "3", statusId = "104", reblogAuthorServerId = "2"),
|
||||
// different Tusky Account
|
||||
fakeHomeTimelineData(id = "9", tuskyAccountId = 2, authorServerId = "3", statusId = "105", reblogAuthorServerId = "1"),
|
||||
)
|
||||
|
||||
db.insert(statusData - statusData[1] - statusData[8] - statusData [9], tuskyAccountId = 1)
|
||||
db.insert(listOf(statusData[1], statusData[8], statusData [9]), tuskyAccountId = 2)
|
||||
|
||||
timelineDao.removeAllByUser(1, "1")
|
||||
|
||||
val loadedHomeTimelineItems: MutableList<String> = mutableListOf()
|
||||
val accountCursor = db.query("SELECT id FROM HomeTimelineEntity ORDER BY id ASC", null)
|
||||
accountCursor.moveToFirst()
|
||||
while (!accountCursor.isAfterLast) {
|
||||
val id: String = accountCursor.getString(accountCursor.getColumnIndex("id"))
|
||||
loadedHomeTimelineItems.add(id)
|
||||
accountCursor.moveToNext()
|
||||
}
|
||||
accountCursor.close()
|
||||
|
||||
val expectedHomeTimelineItems = listOf("1", "2", "3", "5", "8", "9")
|
||||
|
||||
assertEquals(expectedHomeTimelineItems, loadedHomeTimelineItems)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should correctly delete statuses and reblogs by user`() = runTest {
|
||||
val statusData = listOf(
|
||||
// will be deleted because it is a direct post
|
||||
fakeHomeTimelineData(id = "0", tuskyAccountId = 1, authorServerId = "1"),
|
||||
// different Tusky Account
|
||||
fakeHomeTimelineData(id = "1", tuskyAccountId = 2, authorServerId = "1"),
|
||||
// different author
|
||||
fakeHomeTimelineData(id = "2", tuskyAccountId = 1, authorServerId = "2"),
|
||||
// different author and reblogger
|
||||
fakeHomeTimelineData(id = "3", tuskyAccountId = 1, authorServerId = "2", statusId = "100", reblogAuthorServerId = "3"),
|
||||
// will be deleted because it is a reblog
|
||||
fakeHomeTimelineData(id = "4", tuskyAccountId = 1, authorServerId = "2", statusId = "101", reblogAuthorServerId = "1"),
|
||||
// not a status
|
||||
fakePlaceholderHomeTimelineData(id = "5"),
|
||||
// will be deleted because it is a self reblog
|
||||
fakeHomeTimelineData(id = "6", tuskyAccountId = 1, authorServerId = "1", statusId = "102", reblogAuthorServerId = "1"),
|
||||
// will NOT be deleted because it direct post reblogged by another user
|
||||
fakeHomeTimelineData(id = "7", tuskyAccountId = 1, authorServerId = "1", statusId = "103", reblogAuthorServerId = "3"),
|
||||
// different Tusky Account
|
||||
fakeHomeTimelineData(id = "8", tuskyAccountId = 2, authorServerId = "3", statusId = "104", reblogAuthorServerId = "2"),
|
||||
// different Tusky Account
|
||||
fakeHomeTimelineData(id = "9", tuskyAccountId = 2, authorServerId = "3", statusId = "105", reblogAuthorServerId = "1"),
|
||||
)
|
||||
|
||||
db.insert(statusData - statusData[1] - statusData[8] - statusData [9], tuskyAccountId = 1)
|
||||
db.insert(listOf(statusData[1], statusData[8], statusData [9]), tuskyAccountId = 2)
|
||||
|
||||
timelineDao.removeStatusesAndReblogsByUser(1, "1")
|
||||
|
||||
val loadedHomeTimelineItems: MutableList<String> = mutableListOf()
|
||||
val accountCursor = db.query("SELECT id FROM HomeTimelineEntity ORDER BY id ASC", null)
|
||||
accountCursor.moveToFirst()
|
||||
while (!accountCursor.isAfterLast) {
|
||||
val id: String = accountCursor.getString(accountCursor.getColumnIndex("id"))
|
||||
loadedHomeTimelineItems.add(id)
|
||||
accountCursor.moveToNext()
|
||||
}
|
||||
accountCursor.close()
|
||||
|
||||
val expectedHomeTimelineItems = listOf("1", "2", "3", "5", "7", "8", "9")
|
||||
|
||||
assertEquals(expectedHomeTimelineItems, loadedHomeTimelineItems)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,48 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.keylesspalace.tusky.settings.ProxyConfiguration
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ProxyConfigurationTest {
|
||||
@Test
|
||||
fun `serialized non-int is not valid proxy port`() {
|
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("should fail"))
|
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("1.5"))
|
||||
assertFalse(ProxyConfiguration.isValidProxyPort("should fail"))
|
||||
assertFalse(ProxyConfiguration.isValidProxyPort("1.5"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `number outside port range is not valid`() {
|
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}"))
|
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}"))
|
||||
assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}"))
|
||||
assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `number in port range, inclusive of min and max, is valid`() {
|
||||
Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT))
|
||||
Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT))
|
||||
Assert.assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2))
|
||||
assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT))
|
||||
assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT))
|
||||
assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create with invalid port yields null`() {
|
||||
Assert.assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1))
|
||||
assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create with invalid hostname yields null`() {
|
||||
Assert.assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT))
|
||||
assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create with valid hostname and port yields the config object`() {
|
||||
Assert.assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
|
||||
assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unicode hostname allowed`() {
|
||||
Assert.assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
|
||||
assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.keylesspalace.tusky.json
|
||||
|
||||
import com.keylesspalace.tusky.entity.PreviewCard
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
|
||||
import java.util.Date
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -11,6 +14,7 @@ class GuardedAdapterTest {
|
|||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(GuardedAdapter.ANNOTATION_FACTORY)
|
||||
.add(Date::class.java, Rfc3339DateJsonAdapter())
|
||||
.build()
|
||||
|
||||
@Test
|
||||
|
|
@ -131,4 +135,53 @@ class GuardedAdapterTest {
|
|||
moshi.adapter<Relationship>().fromJson(jsonInput)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize PreviewCard when attribute 'published_at' is invalid`() {
|
||||
// https://github.com/tuskyapp/Tusky/issues/4992
|
||||
val jsonInput = """{
|
||||
|
||||
"url": "https://www.cbc.ca/amp/1.7484477",
|
||||
"title": "Canada reconsidering F-35 purchase amid tensions with Washington, says minister",
|
||||
"description": "Canada is looking at cancelling a major portion of its purchase of U.S.-built F-35 stealth fighters and plans on opening talks with rival aircraft makers, Defence Minister Bill Blair said.",
|
||||
"language": "en",
|
||||
"type": "link",
|
||||
"author_name": "Murray Brewster",
|
||||
"author_url": "",
|
||||
"provider_name": "CBC",
|
||||
"provider_url": "",
|
||||
"html": "",
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"image": "https://files.mastodon.social/cache/preview_cards/images/137/231/445/original/0f63297db3ac7362.jpg",
|
||||
"image_description": "",
|
||||
"embed_url": "",
|
||||
"blurhash": "U74#eeXoK9nLrVWZS+nfXVaenKkXTOjErobx",
|
||||
"published_at": "57171-08-04T06:31:30.000Z",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Murray Brewster",
|
||||
"url": "",
|
||||
"account": null
|
||||
}
|
||||
]
|
||||
|
||||
}"""
|
||||
assertEquals(
|
||||
PreviewCard(
|
||||
url = "https://www.cbc.ca/amp/1.7484477",
|
||||
title = "Canada reconsidering F-35 purchase amid tensions with Washington, says minister",
|
||||
description = "Canada is looking at cancelling a major portion of its purchase of U.S.-built F-35 stealth fighters and plans on opening talks with rival aircraft makers, Defence Minister Bill Blair said.",
|
||||
type = "link",
|
||||
authorName = "Murray Brewster",
|
||||
providerName = "CBC",
|
||||
image = "https://files.mastodon.social/cache/preview_cards/images/137/231/445/original/0f63297db3ac7362.jpg",
|
||||
width = 0,
|
||||
height = 0,
|
||||
blurhash = "U74#eeXoK9nLrVWZS+nfXVaenKkXTOjErobx",
|
||||
publishedAt = null,
|
||||
),
|
||||
moshi.adapter<PreviewCard>().fromJson(jsonInput)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
package com.keylesspalace.tusky.network
|
||||
|
||||
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
class ApiFactoryTest {
|
||||
|
||||
private val mockWebServer = MockWebServer()
|
||||
private val okHttpClient = OkHttpClient.Builder().build()
|
||||
private val moshi = Moshi.Builder().build()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockWebServer.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
private fun retrofit() = Retrofit.Builder()
|
||||
.baseUrl("http://${MastodonApi.PLACEHOLDER_DOMAIN}:${mockWebServer.port}")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun `should make request to the active account's instance`() = runTest {
|
||||
mockInstanceResponse()
|
||||
|
||||
val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = mockWebServer.hostName,
|
||||
accessToken = "fakeToken",
|
||||
clientId = "fakeId",
|
||||
clientSecret = "fakeSecret",
|
||||
isActive = true
|
||||
)
|
||||
|
||||
val retrofit = retrofit()
|
||||
val api: MastodonApi = apiForAccount(account, okHttpClient, retrofit, "http://", mockWebServer.port)
|
||||
|
||||
val instanceResponse = api.getInstance()
|
||||
|
||||
assertTrue(instanceResponse.isSuccess)
|
||||
assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should make request to instance requested in special header when account active`() = runTest {
|
||||
mockInstanceResponse()
|
||||
|
||||
val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = mockWebServer.hostName,
|
||||
accessToken = "fakeToken",
|
||||
clientId = "fakeId",
|
||||
clientSecret = "fakeSecret",
|
||||
isActive = true
|
||||
)
|
||||
|
||||
val retrofit = retrofit()
|
||||
val api: MastodonApi = apiForAccount(account, okHttpClient, retrofit, "http://", mockWebServer.port)
|
||||
|
||||
val instanceResponse = api.getInstance(domain = mockWebServer.hostName)
|
||||
|
||||
assertTrue(instanceResponse.isSuccess)
|
||||
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should make request to instance requested in special header when no account active`() = runTest {
|
||||
mockInstanceResponse()
|
||||
|
||||
val retrofit = retrofit()
|
||||
val api: MastodonApi = apiForAccount(null, okHttpClient, retrofit, "http://", mockWebServer.port)
|
||||
|
||||
val instanceResponse = api.getInstance(domain = mockWebServer.hostName)
|
||||
|
||||
assertTrue(instanceResponse.isSuccess)
|
||||
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail when current instance is requested but no user is logged in`() = runTest {
|
||||
mockInstanceResponse()
|
||||
|
||||
val retrofit = retrofit()
|
||||
val api: MastodonApi = apiForAccount(null, okHttpClient, retrofit, "http://", mockWebServer.port)
|
||||
|
||||
val instanceResponse = api.getInstance()
|
||||
|
||||
assertTrue(instanceResponse.isFailure)
|
||||
assertEquals(0, mockWebServer.requestCount)
|
||||
}
|
||||
|
||||
private fun mockInstanceResponse() {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setBody(
|
||||
moshi.adapter(Instance::class.java).toJson(
|
||||
Instance(
|
||||
domain = "example.org",
|
||||
version = "1.0.0"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
package com.keylesspalace.tusky.network
|
||||
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
class InstanceSwitchAuthInterceptorTest {
|
||||
|
||||
private val mockWebServer = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockWebServer.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should make regular request when requested`() {
|
||||
mockWebServer.enqueue(MockResponse())
|
||||
|
||||
val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doAnswer { null }
|
||||
}
|
||||
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url(mockWebServer.url("/test"))
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
||||
assertEquals(200, response.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should make request to instance requested in special header`() {
|
||||
mockWebServer.enqueue(MockResponse())
|
||||
|
||||
val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doAnswer {
|
||||
AccountEntity(
|
||||
id = 1,
|
||||
domain = "test.domain",
|
||||
accessToken = "fakeToken",
|
||||
clientId = "fakeId",
|
||||
clientSecret = "fakeSecret",
|
||||
isActive = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test")
|
||||
.header(MastodonApi.DOMAIN_HEADER, mockWebServer.hostName)
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
||||
assertEquals(200, response.code)
|
||||
|
||||
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should make request to current instance when requested and user is logged in`() {
|
||||
mockWebServer.enqueue(MockResponse())
|
||||
|
||||
val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doAnswer {
|
||||
AccountEntity(
|
||||
id = 1,
|
||||
domain = mockWebServer.hostName,
|
||||
accessToken = "fakeToken",
|
||||
clientId = "fakeId",
|
||||
clientSecret = "fakeSecret",
|
||||
isActive = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test")
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
||||
assertEquals(200, response.code)
|
||||
|
||||
assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail to make request when request to current instance is requested but no user is logged in`() {
|
||||
mockWebServer.enqueue(MockResponse())
|
||||
|
||||
val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doAnswer { null }
|
||||
}
|
||||
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + "/test")
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
||||
assertEquals(400, response.code)
|
||||
assertEquals(0, mockWebServer.requestCount)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.keylesspalace.tusky.appstore.StatusChangedEvent
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
|
|
@ -21,7 +21,7 @@ import org.robolectric.annotation.Config
|
|||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimelineCasesTest {
|
||||
|
||||
|
|
@ -39,23 +39,21 @@ class TimelineCasesTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `pin success emits StatusChangedEvent`() {
|
||||
fun `pin success emits StatusChangedEvent`() = runTest {
|
||||
val pinnedStatus = mockStatus(pinned = true)
|
||||
|
||||
api.stub {
|
||||
onBlocking { pinStatus(statusId) } doReturn NetworkResult.success(pinnedStatus)
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
eventHub.events.test {
|
||||
timelineCases.pin(statusId, true)
|
||||
assertEquals(StatusChangedEvent(pinnedStatus), awaitItem())
|
||||
}
|
||||
eventHub.events.test {
|
||||
timelineCases.pin(statusId, true)
|
||||
assertEquals(StatusChangedEvent(pinnedStatus), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pin failure with server error throws TimelineError with server message`() {
|
||||
fun `pin failure with server error throws TimelineError with server message`() = runTest {
|
||||
api.stub {
|
||||
onBlocking { pinStatus(statusId) } doReturn NetworkResult.failure(
|
||||
HttpException(
|
||||
|
|
@ -66,12 +64,10 @@ class TimelineCasesTest {
|
|||
)
|
||||
)
|
||||
}
|
||||
runBlocking {
|
||||
assertEquals(
|
||||
"Validation Failed: You have already pinned the maximum number of toots",
|
||||
timelineCases.pin(statusId, true).exceptionOrNull()?.message
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
"Validation Failed: You have already pinned the maximum number of toots",
|
||||
timelineCases.pin(statusId, true).exceptionOrNull()?.message
|
||||
)
|
||||
}
|
||||
|
||||
private fun mockStatus(pinned: Boolean = false): Status {
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* Tusky 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 Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
||||
class FlowExtensionsTest {
|
||||
@Test
|
||||
fun `throttleFirst throttles first`() = runTest {
|
||||
flow {
|
||||
emit(1) // t = 0, emitted
|
||||
delay(90.milliseconds)
|
||||
emit(2) // throttled, t = 90
|
||||
delay(90.milliseconds)
|
||||
emit(3) // throttled, t == 180
|
||||
delay(1010.milliseconds)
|
||||
emit(4) // t = 1190, emitted
|
||||
delay(1010.milliseconds)
|
||||
emit(5) // t = 2200, emitted
|
||||
}
|
||||
.throttleFirst(1000.milliseconds, timeSource = testScheduler.timeSource)
|
||||
.test {
|
||||
advanceUntilIdle()
|
||||
assertThat(awaitItem()).isEqualTo(1)
|
||||
assertThat(awaitItem()).isEqualTo(4)
|
||||
assertThat(awaitItem()).isEqualTo(5)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,8 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class HttpHeaderLinkTest {
|
||||
data class TestData(val name: String, val input: String, val want: List<HttpHeaderLink>)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,17 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LinkHelperTest {
|
||||
private val listener = object : LinkListener {
|
||||
|
|
@ -51,7 +55,7 @@ class LinkHelperTest {
|
|||
|
||||
urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java)
|
||||
for (span in urlSpans) {
|
||||
Assert.assertNotNull(mentions.firstOrNull { it.url == span.url })
|
||||
assertNotNull(mentions.firstOrNull { it.url == span.url })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +76,7 @@ class LinkHelperTest {
|
|||
|
||||
urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java)
|
||||
for (span in urlSpans) {
|
||||
Assert.assertEquals(nonMentionUrl, span.url)
|
||||
assertEquals(nonMentionUrl, span.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +85,8 @@ class LinkHelperTest {
|
|||
for (tag in tags) {
|
||||
for (mutatedTagName in listOf(tag.name, tag.name.uppercase(), tag.name.lowercase())) {
|
||||
val tagName = getTagName("#$mutatedTagName", tags)
|
||||
Assert.assertNotNull(tagName)
|
||||
Assert.assertNotNull(tags.firstOrNull { it.name == tagName })
|
||||
assertNotNull(tagName)
|
||||
assertNotNull(tags.firstOrNull { it.name == tagName })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,22 +97,22 @@ class LinkHelperTest {
|
|||
for (tag in tags) {
|
||||
val mutatedTagName = String(tag.name.map { mutator[it] ?: it }.toCharArray())
|
||||
val tagName = getTagName("#$mutatedTagName", tags)
|
||||
Assert.assertNotNull(tagName)
|
||||
Assert.assertNotNull(tags.firstOrNull { it.name == tagName })
|
||||
assertNotNull(tagName)
|
||||
assertNotNull(tags.firstOrNull { it.name == tagName })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hashedUrlSpans_withNoMatchingTag_areNotModified() {
|
||||
for (tag in tags) {
|
||||
Assert.assertNull(getTagName("#not${tag.name}", tags))
|
||||
assertNull(getTagName("#not${tag.name}", tags))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTagsAreNull_tagNameIsGeneratedFromText() {
|
||||
for (tag in tags) {
|
||||
Assert.assertEquals(tag.name, getTagName("#${tag.name}", null))
|
||||
assertEquals(tag.name, getTagName("#${tag.name}", null))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +124,7 @@ class LinkHelperTest {
|
|||
"http:/foo.bar",
|
||||
"c:/foo/bar"
|
||||
).forEach {
|
||||
Assert.assertEquals("", getDomain(it))
|
||||
assertEquals("", getDomain(it))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +146,7 @@ class LinkHelperTest {
|
|||
"https://$domain/foo/bar.html?argument=value",
|
||||
"https://$domain/foo/bar.html?argument=value&otherArgument=otherValue"
|
||||
).forEach { url ->
|
||||
Assert.assertEquals(domain, getDomain(url))
|
||||
assertEquals(domain, getDomain(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +159,7 @@ class LinkHelperTest {
|
|||
"http://www.localhost" to "localhost",
|
||||
"https://wwwexample.com/" to "wwwexample.com"
|
||||
).forEach { (url, domain) ->
|
||||
Assert.assertEquals(domain, getDomain(url))
|
||||
assertEquals(domain, getDomain(url))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,11 +171,11 @@ class LinkHelperTest {
|
|||
val content = SpannableStringBuilder()
|
||||
content.append(displayedContent, URLSpan(maliciousUrl), 0)
|
||||
val oldContent = content.toString()
|
||||
Assert.assertEquals(
|
||||
textView.context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain),
|
||||
assertEquals(
|
||||
displayedContent + " " + textView.context.getString(R.string.url_domain_notifier, maliciousDomain),
|
||||
markupHiddenUrls(textView, content).toString()
|
||||
)
|
||||
Assert.assertEquals(oldContent, content.toString())
|
||||
assertEquals(oldContent, content.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -181,8 +185,8 @@ class LinkHelperTest {
|
|||
val maliciousUrl = "https://$maliciousDomain/to/go"
|
||||
val content = SpannableStringBuilder()
|
||||
content.append(displayedContent, URLSpan(maliciousUrl), 0)
|
||||
Assert.assertEquals(
|
||||
textView.context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain),
|
||||
assertEquals(
|
||||
displayedContent + " " + textView.context.getString(R.string.url_domain_notifier, maliciousDomain),
|
||||
markupHiddenUrls(textView, content).toString()
|
||||
)
|
||||
}
|
||||
|
|
@ -198,7 +202,7 @@ class LinkHelperTest {
|
|||
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
for (domain in domains) {
|
||||
Assert.assertTrue(markedUpContent.contains(textView.context.getString(R.string.url_domain_notifier, displayedContent, domain)))
|
||||
assertTrue(markedUpContent.contains(displayedContent + " " + textView.context.getString(R.string.url_domain_notifier, domain)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +220,7 @@ class LinkHelperTest {
|
|||
.append("$domain/", URLSpan("https://www.$domain"), 0)
|
||||
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
Assert.assertFalse(markedUpContent.contains("🔗"))
|
||||
assertFalse(markedUpContent.contains("🔗"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -229,7 +233,7 @@ class LinkHelperTest {
|
|||
.append("Some Place https://some.place/path", URLSpan("https://some.place/path"), 0)
|
||||
|
||||
val markedUpContent = markupHiddenUrls(textView, content)
|
||||
Assert.assertFalse(markedUpContent.contains("🔗"))
|
||||
assertFalse(markedUpContent.contains("🔗"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -249,8 +253,9 @@ class LinkHelperTest {
|
|||
"Another Place | https://another.place/",
|
||||
"Another Place https://another.place/path"
|
||||
)
|
||||
print(markedUpContent)
|
||||
asserts.forEach {
|
||||
Assert.assertTrue(markedUpContent.contains(textView.context.getString(R.string.url_domain_notifier, it, "some.place")))
|
||||
assertTrue(markedUpContent.contains(it + " " + textView.context.getString(R.string.url_domain_notifier, "some.place")))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +269,7 @@ class LinkHelperTest {
|
|||
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (mention in mentions) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
|
||||
assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +283,7 @@ class LinkHelperTest {
|
|||
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (mention in mentions) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
|
||||
assertFalse(markedUpContent.contains("${getDomain(mention.url)})"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -292,7 +297,7 @@ class LinkHelperTest {
|
|||
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (tag in tags) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
|
||||
assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +311,90 @@ class LinkHelperTest {
|
|||
|
||||
val markedUpContent = markupHiddenUrls(textView, builder)
|
||||
for (tag in tags) {
|
||||
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
|
||||
assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags with empty content returns empty list`() {
|
||||
val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder(""))
|
||||
assertEquals(0, endOfContent)
|
||||
assert(trailingHashtags.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags with no hashtags returns empty list`() {
|
||||
val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("some untagged content"))
|
||||
assertEquals(21, endOfContent)
|
||||
assert(trailingHashtags.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags with all inline hashtags returns empty list`() {
|
||||
val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("some #inline #tagged #content"))
|
||||
assertEquals(29, endOfContent)
|
||||
assert(trailingHashtags.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags with only hashtags returns empty list`() {
|
||||
val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("#some #inline #tagged #content"))
|
||||
assertEquals(0, endOfContent)
|
||||
assert(trailingHashtags.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags with one tag`() {
|
||||
val content = SpannableStringBuilder("some content followed by tags:\n").apply {
|
||||
tags.first().let { append("#${it.name}", URLSpan(it.url), 0) }
|
||||
}
|
||||
|
||||
val (endOfContent, trailingHashtags) = getTrailingHashtags(content)
|
||||
assertEquals(30, endOfContent)
|
||||
assertEquals(tags.first().name, trailingHashtags.single().name)
|
||||
assertEquals(tags.first().url, trailingHashtags.single().url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags with multiple tags`() {
|
||||
for (separator in listOf(" ", "\t", "\n", "\r\n")) {
|
||||
val content = SpannableStringBuilder("some content followed by tags:\n").apply {
|
||||
for (tag in tags) {
|
||||
append(separator)
|
||||
append("#${tag.name}", URLSpan(tag.url), 0)
|
||||
append(separator)
|
||||
}
|
||||
}
|
||||
|
||||
val (endOfContent, trailingHashtags) = getTrailingHashtags(content)
|
||||
assertEquals(30, endOfContent)
|
||||
assertEquals(tags.size, trailingHashtags.size)
|
||||
tags.forEachIndexed { index, tag ->
|
||||
assertEquals(tag.name, trailingHashtags[index].name)
|
||||
assertEquals(tag.url, trailingHashtags[index].url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get trailing hashtags ignores inline tags`() {
|
||||
for (separator in listOf(" ", "\t", "\n", "\r\n")) {
|
||||
val content = SpannableStringBuilder("some content with inline tag ").apply {
|
||||
append("#inline", URLSpan("https://example.com/tag/inline"), 0)
|
||||
append(" followed by trailing tags\n")
|
||||
for (tag in tags) {
|
||||
append(separator)
|
||||
append("#${tag.name}", URLSpan(tag.url), 0)
|
||||
append(separator)
|
||||
}
|
||||
}
|
||||
|
||||
val (_, trailingHashtags) = getTrailingHashtags(content)
|
||||
assertEquals(tags.size, trailingHashtags.size)
|
||||
tags.forEachIndexed { index, tag ->
|
||||
assertEquals(tag.name, trailingHashtags[index].name)
|
||||
assertEquals(tag.url, trailingHashtags[index].url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -315,7 +403,7 @@ class LinkHelperTest {
|
|||
companion object {
|
||||
@Parameterized.Parameters(name = "match_{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
fun data(): Iterable<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf("https://mastodon.foo.bar/@User", true),
|
||||
arrayOf("http://mastodon.foo.bar/@abc123", true),
|
||||
|
|
@ -375,7 +463,7 @@ class LinkHelperTest {
|
|||
|
||||
@Test
|
||||
fun test() {
|
||||
Assert.assertEquals(expectedResult, looksLikeMastodonUrl(url))
|
||||
assertEquals(expectedResult, looksLikeMastodonUrl(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,46 +3,48 @@ package com.keylesspalace.tusky.util
|
|||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import org.junit.Assert
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LocaleUtilsTest {
|
||||
@Test
|
||||
fun initialLanguagesContainReplySelectedAppAndSystem() {
|
||||
val expectedLanguages = arrayOf<String?>("yi", "tok", "da", "fr", "sv", "kab")
|
||||
val languages = getMockedInitialLanguages(expectedLanguages)
|
||||
Assert.assertArrayEquals(expectedLanguages, languages.subList(0, expectedLanguages.size).toTypedArray())
|
||||
assertArrayEquals(expectedLanguages, languages.subList(0, expectedLanguages.size).toTypedArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenReplyLanguageIsNull_DefaultLanguageIsFirst() {
|
||||
val defaultLanguage = "tok"
|
||||
val languages = getMockedInitialLanguages(arrayOf(null, defaultLanguage, "da", "fr", "sv", "kab"))
|
||||
Assert.assertEquals(defaultLanguage, languages[0])
|
||||
assertEquals(defaultLanguage, languages[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialLanguagesAreDistinct() {
|
||||
val defaultLanguage = "da"
|
||||
val languages = getMockedInitialLanguages(arrayOf(defaultLanguage, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))
|
||||
Assert.assertEquals(1, languages.count { it == defaultLanguage })
|
||||
assertEquals(1, languages.count { it == defaultLanguage })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialLanguageDeduplicationDoesNotReorder() {
|
||||
val defaultLanguage = "da"
|
||||
|
||||
Assert.assertEquals(
|
||||
assertEquals(
|
||||
defaultLanguage,
|
||||
getMockedInitialLanguages(arrayOf(defaultLanguage, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))[0]
|
||||
)
|
||||
Assert.assertEquals(
|
||||
assertEquals(
|
||||
defaultLanguage,
|
||||
getMockedInitialLanguages(arrayOf(null, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))[0]
|
||||
)
|
||||
|
|
@ -51,7 +53,7 @@ class LocaleUtilsTest {
|
|||
@Test
|
||||
fun emptyInitialLanguagesAreDropped() {
|
||||
val languages = getMockedInitialLanguages(arrayOf("", "", "fr", "", "kab", ""))
|
||||
Assert.assertFalse(languages.any { it.isEmpty() })
|
||||
assertFalse(languages.any { it.isEmpty() })
|
||||
}
|
||||
|
||||
private fun getMockedInitialLanguages(configuredLanguages: Array<String?>): List<String> {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.keylesspalace.tusky.util
|
|||
import java.util.Locale
|
||||
import kotlin.math.pow
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -35,7 +35,7 @@ class NumberUtilsTest(private val input: Long, private val want: String) {
|
|||
|
||||
@Parameterized.Parameters(name = "formatNumber_{0}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Any> {
|
||||
fun data(): Iterable<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf(0, "0"),
|
||||
arrayOf(1, "1"),
|
||||
|
|
@ -65,6 +65,6 @@ class NumberUtilsTest(private val input: Long, private val want: String) {
|
|||
|
||||
@Test
|
||||
fun test() {
|
||||
Assert.assertEquals(want, formatNumber(input, 1000))
|
||||
assertEquals(want, formatNumber(input, 1000))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import org.junit.runner.RunWith
|
|||
import org.robolectric.Robolectric
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RickRollTest {
|
||||
private lateinit var activity: Activity
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmartLengthInputFilterTest {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,78 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.R
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
private const val STATUS_CREATED_AT_NOW = "test"
|
||||
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [34])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimestampUtilsTest {
|
||||
private val ctx: Context = mock {
|
||||
on { getString(R.string.status_created_at_now) } doReturn STATUS_CREATED_AT_NOW
|
||||
|
||||
companion object {
|
||||
private lateinit var locale: Locale
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun beforeClass() {
|
||||
locale = Locale.getDefault()
|
||||
Locale.setDefault(Locale.ENGLISH)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun afterClass() {
|
||||
Locale.setDefault(locale)
|
||||
}
|
||||
}
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Test
|
||||
fun `should return 'now' for small timespans`() {
|
||||
assertEquals("now", getRelativeTimeSpanString(context, 0, 300))
|
||||
assertEquals("now", getRelativeTimeSpanString(context, 300, 0))
|
||||
assertEquals("now", getRelativeTimeSpanString(context, 501, 0))
|
||||
assertEquals("now", getRelativeTimeSpanString(context, 0, 999))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldShowNowForSmallTimeSpans() {
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0))
|
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999))
|
||||
fun `should return 'in --' when then is after now`() {
|
||||
assertEquals("in 49s", getRelativeTimeSpanString(context, 49.seconds.inWholeMilliseconds, 0))
|
||||
assertEquals("in 34m", getRelativeTimeSpanString(context, 37.minutes.inWholeMilliseconds, 3.minutes.inWholeMilliseconds))
|
||||
assertEquals("in 7h", getRelativeTimeSpanString(context, 10.hours.inWholeMilliseconds, 3.hours.inWholeMilliseconds))
|
||||
assertEquals("in 10d", getRelativeTimeSpanString(context, 10.days.inWholeMilliseconds, 0))
|
||||
assertEquals("in 4y", getRelativeTimeSpanString(context, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds, 800.days.inWholeMilliseconds))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct timespans`() {
|
||||
assertEquals("49s", getRelativeTimeSpanString(context, 0, 49.seconds.inWholeMilliseconds))
|
||||
assertEquals("34m", getRelativeTimeSpanString(context, 3.minutes.inWholeMilliseconds, 37.minutes.inWholeMilliseconds))
|
||||
assertEquals("7h", getRelativeTimeSpanString(context, 3.hours.inWholeMilliseconds, 10.hours.inWholeMilliseconds))
|
||||
assertEquals("10d", getRelativeTimeSpanString(context, 0, 10.days.inWholeMilliseconds))
|
||||
assertEquals("4y", getRelativeTimeSpanString(context, 800.days.inWholeMilliseconds, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return correct poll duration`() {
|
||||
assertEquals("1 second left", formatPollDuration(context, 1.seconds.inWholeMilliseconds, 0))
|
||||
assertEquals("49 seconds left", formatPollDuration(context, 49.seconds.inWholeMilliseconds, 0))
|
||||
assertEquals("1 minute left", formatPollDuration(context, 37.minutes.inWholeMilliseconds, 36.minutes.inWholeMilliseconds))
|
||||
assertEquals("34 minutes left", formatPollDuration(context, 37.minutes.inWholeMilliseconds, 3.minutes.inWholeMilliseconds))
|
||||
assertEquals("1 hour left", formatPollDuration(context, 10.hours.inWholeMilliseconds, 9.hours.inWholeMilliseconds))
|
||||
assertEquals("7 hours left", formatPollDuration(context, 10.hours.inWholeMilliseconds, 3.hours.inWholeMilliseconds))
|
||||
assertEquals("1 day left", formatPollDuration(context, 1.days.inWholeMilliseconds, 0))
|
||||
assertEquals("10 days left", formatPollDuration(context, 10.days.inWholeMilliseconds, 0))
|
||||
assertEquals("1460 days left", formatPollDuration(context, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds, 800.days.inWholeMilliseconds))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue