Fix incorrectly incrementing IDs before sending to server. (#1026)
* Fix incorrectly incrementing IDs before sending to server. * Add TimelineRepositoryTest, fix adding placeholder, fix String#dec() * Add more TimelineRepository tests, fix bugs * Add tests for adding statuses from DB.
This commit is contained in:
parent
85610a8311
commit
63952813c8
11 changed files with 631 additions and 208 deletions
50
app/src/test/java/android/text/FakeSpannableString.kt
Normal file
50
app/src/test/java/android/text/FakeSpannableString.kt
Normal file
|
@ -0,0 +1,50 @@
|
|||
package android.text
|
||||
|
||||
// Used for stubbing Android implementation without slow & buggy Robolectric things
|
||||
@Suppress("unused")
|
||||
class SpannableString(private val text: CharSequence) : Spannable {
|
||||
|
||||
override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun <T : Any?> getSpans(start: Int, end: Int, type: Class<T>?): Array<T> {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun removeSpan(what: Any?) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "FakeSpannableString[text=$text]"
|
||||
}
|
||||
|
||||
override val length: Int
|
||||
get() = text.length
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import com.keylesspalace.tusky.util.dec
|
||||
import com.keylesspalace.tusky.util.inc
|
||||
import com.keylesspalace.tusky.util.isLessThan
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
@ -11,7 +12,8 @@ class StringUtilsTest {
|
|||
val lessList = listOf(
|
||||
"abc" to "bcd",
|
||||
"ab" to "abc",
|
||||
"cb" to "abc"
|
||||
"cb" to "abc",
|
||||
"1" to "2"
|
||||
)
|
||||
lessList.forEach { (l, r) -> assertTrue("$l < $r", l.isLessThan(r)) }
|
||||
val notLessList = lessList.map { (l, r) -> r to l } + listOf(
|
||||
|
@ -20,6 +22,15 @@ class StringUtilsTest {
|
|||
notLessList.forEach { (l, r) -> assertFalse("not $l < $r", l.isLessThan(r)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun inc() {
|
||||
listOf(
|
||||
"122" to "123",
|
||||
"12A" to "12B",
|
||||
"1" to "2"
|
||||
).forEach { (l, r) -> assertEquals("$l + 1 = $r", r, l.inc()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dec() {
|
||||
listOf(
|
||||
|
@ -28,7 +39,8 @@ class StringUtilsTest {
|
|||
"120" to "11z",
|
||||
"100" to "zz",
|
||||
"0" to "",
|
||||
"" to ""
|
||||
"" to "",
|
||||
"2" to "1"
|
||||
).forEach { (l, r) -> assertEquals("$l - 1 = $r", r, l.dec()) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.text.Spanned
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.SpanUtilsTest
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.TimelineDao
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.repository.*
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.HtmlConverter
|
||||
import com.nhaarman.mockitokotlin2.isNull
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TimelineRepositoryTest {
|
||||
@Mock
|
||||
lateinit var timelineDao: TimelineDao
|
||||
|
||||
@Mock
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
|
||||
@Mock
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
lateinit var gson: Gson
|
||||
|
||||
lateinit var subject: TimelineRepository
|
||||
|
||||
lateinit var testScheduler: TestScheduler
|
||||
|
||||
|
||||
val limit = 30
|
||||
val account = AccountEntity(
|
||||
id = 2,
|
||||
accessToken = "token",
|
||||
domain = "domain.com",
|
||||
isActive = true
|
||||
)
|
||||
val htmlConverter = object : HtmlConverter {
|
||||
override fun fromHtml(html: String): Spanned {
|
||||
return SpanUtilsTest.FakeSpannable(html)
|
||||
}
|
||||
|
||||
override fun toHtml(text: Spanned): String {
|
||||
return text.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(accountManager.activeAccount).thenReturn(account)
|
||||
|
||||
gson = Gson()
|
||||
testScheduler = TestScheduler()
|
||||
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
|
||||
subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson,
|
||||
htmlConverter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkUnbounded() {
|
||||
val statuses = listOf(
|
||||
makeStatus("3"),
|
||||
makeStatus("2")
|
||||
)
|
||||
whenever(mastodonApi.homeTimelineSingle(isNull(), isNull(), anyInt()))
|
||||
.thenReturn(Single.just(statuses))
|
||||
val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.NETWORK)
|
||||
.blockingGet()
|
||||
|
||||
assertEquals(statuses.map(Status::lift), result)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
|
||||
verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id))
|
||||
for (status in statuses) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, account.domain, htmlConverter, gson),
|
||||
status.account.toEntity(account.domain, account.id, gson),
|
||||
null
|
||||
)
|
||||
}
|
||||
verifyNoMoreInteractions(timelineDao)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkLoadingTopNoGap() {
|
||||
val response = listOf(
|
||||
makeStatus("4"),
|
||||
makeStatus("3"),
|
||||
makeStatus("2")
|
||||
)
|
||||
val sinceId = "2"
|
||||
val sinceIdMinusOne = "1"
|
||||
whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1))
|
||||
.thenReturn(Single.just(response))
|
||||
val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit,
|
||||
TimelineRequestMode.NETWORK)
|
||||
.blockingGet()
|
||||
|
||||
assertEquals(
|
||||
response.subList(0, 2).map(Status::lift),
|
||||
result
|
||||
)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
|
||||
// We assume for now that overlapped one is inserted but it's not that important
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, account.domain, htmlConverter, gson),
|
||||
status.account.toEntity(account.domain, account.id, gson),
|
||||
null
|
||||
)
|
||||
}
|
||||
verify(timelineDao).removeAllPlaceholdersBetween(account.id, response.first().id,
|
||||
response.last().id)
|
||||
verifyNoMoreInteractions(timelineDao)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkLoadingTopWithGap() {
|
||||
val response = listOf(
|
||||
makeStatus("5"),
|
||||
makeStatus("4")
|
||||
)
|
||||
val sinceId = "2"
|
||||
val sinceIdMinusOne = "1"
|
||||
whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1))
|
||||
.thenReturn(Single.just(response))
|
||||
val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit,
|
||||
TimelineRequestMode.NETWORK)
|
||||
.blockingGet()
|
||||
|
||||
val placeholder = Placeholder("3")
|
||||
assertEquals(response.map(Status::lift) + Either.Left(placeholder), result)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, account.domain, htmlConverter, gson),
|
||||
status.account.toEntity(account.domain, account.id, gson),
|
||||
null
|
||||
)
|
||||
}
|
||||
verify(timelineDao).insertStatusIfNotThere(placeholder.toEntity(account.id))
|
||||
verifyNoMoreInteractions(timelineDao)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkLoadingMiddleNoGap() {
|
||||
// Example timelne:
|
||||
// 5
|
||||
// 4
|
||||
// [gap]
|
||||
// 2
|
||||
// 1
|
||||
|
||||
val response = listOf(
|
||||
makeStatus("5"),
|
||||
makeStatus("4"),
|
||||
makeStatus("3"),
|
||||
makeStatus("2")
|
||||
)
|
||||
val sinceId = "2"
|
||||
val sinceIdMinusOne = "1"
|
||||
val maxId = "3"
|
||||
whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1))
|
||||
.thenReturn(Single.just(response))
|
||||
val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit,
|
||||
TimelineRequestMode.NETWORK)
|
||||
.blockingGet()
|
||||
|
||||
assertEquals(
|
||||
response.subList(0, response.lastIndex).map(Status::lift),
|
||||
result
|
||||
)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
|
||||
// We assume for now that overlapped one is inserted but it's not that important
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, account.domain, htmlConverter, gson),
|
||||
status.account.toEntity(account.domain, account.id, gson),
|
||||
null
|
||||
)
|
||||
}
|
||||
verify(timelineDao).removeAllPlaceholdersBetween(account.id, response.first().id,
|
||||
response.last().id)
|
||||
verifyNoMoreInteractions(timelineDao)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkLoadingMiddleWithGap() {
|
||||
// Example timelne:
|
||||
// 6
|
||||
// 5
|
||||
// [gap]
|
||||
// 2
|
||||
// 1
|
||||
|
||||
val response = listOf(
|
||||
makeStatus("6"),
|
||||
makeStatus("5"),
|
||||
makeStatus("4")
|
||||
)
|
||||
val sinceId = "2"
|
||||
val sinceIdMinusOne = "1"
|
||||
val maxId = "4"
|
||||
whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1))
|
||||
.thenReturn(Single.just(response))
|
||||
val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit,
|
||||
TimelineRequestMode.NETWORK)
|
||||
.blockingGet()
|
||||
|
||||
val placeholder = Placeholder("3")
|
||||
assertEquals(
|
||||
response.map(Status::lift) + Either.Left(placeholder),
|
||||
result
|
||||
)
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
|
||||
// We assume for now that overlapped one is inserted but it's not that important
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, account.domain, htmlConverter, gson),
|
||||
status.account.toEntity(account.domain, account.id, gson),
|
||||
null
|
||||
)
|
||||
}
|
||||
verify(timelineDao).removeAllPlaceholdersBetween(account.id, response.first().id,
|
||||
response.last().id)
|
||||
verify(timelineDao).insertStatusIfNotThere(placeholder.toEntity(account.id))
|
||||
verifyNoMoreInteractions(timelineDao)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addingFromDb() {
|
||||
RxJavaPlugins.setIoSchedulerHandler { Schedulers.single() }
|
||||
val status = makeStatus("2")
|
||||
val dbStatus = makeStatus("1")
|
||||
val dbResult = TimelineStatusWithAccount()
|
||||
dbResult.status = dbStatus.toEntity(account.id, account.domain, htmlConverter, gson)
|
||||
dbResult.account = status.account.toEntity(account.domain, account.id, gson)
|
||||
|
||||
whenever(mastodonApi.homeTimelineSingle(any(), any(), any()))
|
||||
.thenReturn(Single.just(listOf(status)))
|
||||
whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30))
|
||||
.thenReturn(Single.just(listOf(dbResult)))
|
||||
val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY)
|
||||
.blockingGet()
|
||||
assertEquals(listOf(status, dbStatus).map(Status::lift), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addingFromDbExhausted() {
|
||||
RxJavaPlugins.setIoSchedulerHandler { Schedulers.single() }
|
||||
val status = makeStatus("4")
|
||||
val dbResult = TimelineStatusWithAccount()
|
||||
dbResult.status = Placeholder("2").toEntity(account.id)
|
||||
val dbResult2 = TimelineStatusWithAccount()
|
||||
dbResult2.status = Placeholder("1").toEntity(account.id)
|
||||
|
||||
whenever(mastodonApi.homeTimelineSingle(any(), any(), any()))
|
||||
.thenReturn(Single.just(listOf(status)))
|
||||
whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30))
|
||||
.thenReturn(Single.just(listOf(dbResult, dbResult2)))
|
||||
val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY)
|
||||
.blockingGet()
|
||||
assertEquals(listOf(status).map(Status::lift), result)
|
||||
}
|
||||
|
||||
private fun makeStatus(id: String, account: Account = makeAccount(id)): Status {
|
||||
return Status(
|
||||
id = id,
|
||||
account = account,
|
||||
content = SpanUtilsTest.FakeSpannable("hello$id"),
|
||||
createdAt = Date(),
|
||||
emojis = listOf(),
|
||||
reblogsCount = 3,
|
||||
favouritesCount = 5,
|
||||
sensitive = false,
|
||||
visibility = Status.Visibility.PUBLIC,
|
||||
spoilerText = "",
|
||||
reblogged = true,
|
||||
favourited = false,
|
||||
attachments = listOf(),
|
||||
mentions = arrayOf(),
|
||||
application = null,
|
||||
inReplyToAccountId = null,
|
||||
inReplyToId = null,
|
||||
pinned = false,
|
||||
reblog = null,
|
||||
url = "http://example.com/statuses/$id"
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeAccount(id: String): Account {
|
||||
return Account(
|
||||
id = id,
|
||||
localUsername = "test$id",
|
||||
username = "test$id@example.com",
|
||||
displayName = "Example Account $id",
|
||||
note = SpanUtilsTest.FakeSpannable("Note! $id"),
|
||||
url = "https://example.com/@test$id",
|
||||
avatar = "avatar$id",
|
||||
header = "Header$id",
|
||||
followersCount = 300,
|
||||
followingCount = 400,
|
||||
statusesCount = 1000,
|
||||
bot = false,
|
||||
emojis = listOf(),
|
||||
fields = null,
|
||||
source = null
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue