fix deleted status reappearing in the timeline (#1225)

* fix deleted status reappearing in the timeline

* fix crash

* fix tests

* fix instrumented tests

* add test for deleted status in timeline
This commit is contained in:
Konrad Pozniak 2019-05-01 22:10:00 +02:00 committed by GitHub
parent 53696f752a
commit cb82202d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 65 additions and 7 deletions

View File

@ -133,6 +133,7 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
}) })
androidTestImplementation 'android.arch.persistence.room:testing:1.1.1' androidTestImplementation 'android.arch.persistence.room:testing:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
testImplementation 'androidx.test.ext:junit:1.1.0' testImplementation 'androidx.test.ext:junit:1.1.0'
debugImplementation 'im.dino:dbinspector:3.4.1@aar' debugImplementation 'im.dino:dbinspector:3.4.1@aar'
implementation 'io.reactivex.rxjava2:rxjava:2.2.8' implementation 'io.reactivex.rxjava2:rxjava:2.2.8'

View File

@ -2,8 +2,8 @@ package com.keylesspalace.tusky
import androidx.room.testing.MigrationTestHelper import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule

View File

@ -123,6 +123,42 @@ class TimelineDAOTest {
) )
} }
@Test
fun overwriteDeletedStatus() {
val oldStatuses = listOf(
makeStatus(statusId = 3),
makeStatus(statusId = 2),
makeStatus(statusId = 1)
)
timelineDao.deleteRange(1, oldStatuses.last().first.serverId, oldStatuses.first().first.serverId)
for ((status, author, reblogAuthor) in oldStatuses) {
timelineDao.insertInTransaction(status, author, reblogAuthor)
}
// status 2 gets deleted, newly loaded status contain only 1 + 3
val newStatuses = listOf(
makeStatus(statusId = 3),
makeStatus(statusId = 1)
)
timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId)
for ((status, author, reblogAuthor) in newStatuses) {
timelineDao.insertInTransaction(status, author, reblogAuthor)
}
//make sure status 2 is no longer in db
assertEquals(
newStatuses,
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() }
)
}
private fun makeStatus( private fun makeStatus(
accountId: Long = 1, accountId: Long = 1,
statusId: Long = 10, statusId: Long = 10,
@ -177,7 +213,8 @@ class TimelineDAOTest {
mentions = "mentions$accountId", mentions = "mentions$accountId",
application = "application$accountId", application = "application$accountId",
reblogServerId = if (reblog) (statusId * 100).toString() else null, reblogServerId = if (reblog) (statusId * 100).toString() else null,
reblogAccountId = reblogAuthor?.serverId reblogAccountId = reblogAuthor?.serverId,
poll = null
) )
return Triple(status, author, reblogAuthor) return Triple(status, author, reblogAuthor)
} }
@ -204,8 +241,8 @@ class TimelineDAOTest {
mentions = null, mentions = null,
application = null, application = null,
reblogServerId = null, reblogServerId = null,
reblogAccountId = null reblogAccountId = null,
poll = null
) )
} }

View File

@ -14,7 +14,6 @@ abstract class TimelineDao {
@Insert(onConflict = REPLACE) @Insert(onConflict = REPLACE)
abstract fun insertAccount(timelineAccountEntity: TimelineAccountEntity): Long abstract fun insertAccount(timelineAccountEntity: TimelineAccountEntity): Long
@Insert(onConflict = REPLACE) @Insert(onConflict = REPLACE)
abstract fun insertStatus(timelineAccountEntity: TimelineStatusEntity): Long abstract fun insertStatus(timelineAccountEntity: TimelineStatusEntity): Long
@ -58,13 +57,20 @@ LIMIT :limit""")
insertStatus(status) insertStatus(status)
} }
@Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
(LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId)
AND
(LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId > :minId)
""")
abstract fun deleteRange(accountId: Long, minId: String, maxId: String)
@Query("""DELETE FROM TimelineStatusEntity WHERE authorServerId = null @Query("""DELETE FROM TimelineStatusEntity WHERE authorServerId = null
AND timelineUserId = :acccount AND AND timelineUserId = :account AND
(LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId) (LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId)
AND AND
(LENGTH(serverId) > LENGTH(:sinceId) OR LENGTH(serverId) == LENGTH(:sinceId) AND serverId > :sinceId) (LENGTH(serverId) > LENGTH(:sinceId) OR LENGTH(serverId) == LENGTH(:sinceId) AND serverId > :sinceId)
""") """)
abstract fun removeAllPlaceholdersBetween(acccount: Long, maxId: String, sinceId: String) abstract fun removeAllPlaceholdersBetween(account: Long, maxId: String, sinceId: String)
@Query("""UPDATE TimelineStatusEntity SET favourited = :favourited @Query("""UPDATE TimelineStatusEntity SET favourited = :favourited
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId - :statusId)""") WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId - :statusId)""")

View File

@ -144,6 +144,11 @@ class TimelineRepositoryImpl(
} }
Single.fromCallable { Single.fromCallable {
if(statuses.isNotEmpty()) {
timelineDao.deleteRange(accountId, statuses.last().id, statuses.first().id)
}
for (status in statuses) { for (status in statuses) {
timelineDao.insertInTransaction( timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson), status.toEntity(accountId, htmlConverter, gson),

View File

@ -91,6 +91,9 @@ class TimelineRepositoryTest {
assertEquals(statuses.map(Status::lift), result) assertEquals(statuses.map(Status::lift), result)
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
verify(timelineDao).deleteRange(account.id, statuses.last().id, statuses.first().id)
verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id)) verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id))
for (status in statuses) { for (status in statuses) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
@ -122,6 +125,7 @@ class TimelineRepositoryTest {
result result
) )
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
@ -152,6 +156,7 @@ class TimelineRepositoryTest {
val placeholder = Placeholder("3") val placeholder = Placeholder("3")
assertEquals(response.map(Status::lift) + Either.Left(placeholder), result) assertEquals(response.map(Status::lift) + Either.Left(placeholder), result)
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, htmlConverter, gson),
@ -192,6 +197,7 @@ class TimelineRepositoryTest {
result result
) )
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
@ -235,6 +241,9 @@ class TimelineRepositoryTest {
) )
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
// We assume for now that overlapped one is inserted but it's not that important // We assume for now that overlapped one is inserted but it's not that important
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
for (status in response) { for (status in response) {
verify(timelineDao).insertInTransaction( verify(timelineDao).insertInTransaction(
status.toEntity(account.id, htmlConverter, gson), status.toEntity(account.id, htmlConverter, gson),