Refactor notifications to Kotlin & paging (#4026)
This refactors the NotificationsFragment and related classes to Kotlin & paging. While trying to preserve as much of the original behavior as possible, this adds the following improvements as well: - The "show notifications filter" preference was added again - The "load more" button now has a background ripple effect when clicked - The "legal" report category of Mastodon 4.2 is now supported in report notifications - Unknown notifications now display "unknown notification type" instead of an empty line Other code quality improvements: - All views from xml layouts are now referenced via ViewBindings - the classes responsible for showing system notifications were moved to a new package `systemnotifications` while the classes from this refactoring are in `notifications` - the id of the local Tusky account is now called `tuskyAccountId` in all places I could find closes https://github.com/tuskyapp/Tusky/issues/3429 --------- Co-authored-by: Zongle Wang <wangzongler@gmail.com>
This commit is contained in:
parent
3bbf96b057
commit
b2c0b18c8e
121 changed files with 6992 additions and 4654 deletions
|
|
@ -11,8 +11,8 @@ 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.NotificationHelper
|
||||
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
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
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)
|
||||
)
|
||||
|
||||
fun Placeholder.toNotificationDataEntity(
|
||||
tuskyAccountId: Long
|
||||
) = NotificationDataEntity(
|
||||
tuskyAccountId = tuskyAccountId,
|
||||
type = null,
|
||||
id = id,
|
||||
account = null,
|
||||
status = null,
|
||||
statusAccount = null,
|
||||
report = null,
|
||||
reportTargetAccount = 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,
|
||||
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,544 @@
|
|||
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.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.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 = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NotificationsRemoteMediatorTest {
|
||||
|
||||
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()
|
||||
|
||||
@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(
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
|
||||
},
|
||||
db = db,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
|
||||
},
|
||||
db = db,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
accountManager = accountManager,
|
||||
api = mock(),
|
||||
db = db,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
accountManager = accountManager,
|
||||
api = mock {
|
||||
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(
|
||||
listOf(
|
||||
fakeNotification(id = "8"),
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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").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(
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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(
|
||||
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,
|
||||
excludes = emptySet()
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
@ -11,15 +11,15 @@ 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.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.test.runTest
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
@ -75,17 +75,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,
|
||||
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 +93,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,
|
||||
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 +110,18 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun `should not prepend statuses`() {
|
||||
fun `should not prepend statuses`() = runTest {
|
||||
val remoteMediator = CachedTimelineRemoteMediator(
|
||||
accountManager = accountManager,
|
||||
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 +129,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,11 +137,11 @@ 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)
|
||||
|
|
@ -154,21 +151,20 @@ class CachedTimelineRemoteMediatorTest {
|
|||
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,32 +178,30 @@ 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)
|
||||
|
|
@ -217,21 +211,20 @@ class CachedTimelineRemoteMediatorTest {
|
|||
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,30 +237,30 @@ 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)
|
||||
|
|
@ -277,21 +270,20 @@ class CachedTimelineRemoteMediatorTest {
|
|||
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 +297,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,
|
||||
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,27 +340,27 @@ 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)
|
||||
|
|
@ -381,13 +372,12 @@ class CachedTimelineRemoteMediatorTest {
|
|||
|
||||
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,27 +390,27 @@ 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)
|
||||
|
|
@ -430,20 +420,19 @@ class CachedTimelineRemoteMediatorTest {
|
|||
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,29 +445,29 @@ 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)
|
||||
|
|
@ -488,14 +477,13 @@ class CachedTimelineRemoteMediatorTest {
|
|||
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 +496,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,
|
||||
|
|
@ -535,41 +523,4 @@ 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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import org.robolectric.annotation.Config
|
|||
@RunWith(AndroidJUnit4::class)
|
||||
class NetworkTimelinePagingSourceTest {
|
||||
|
||||
private val status = mockStatusViewData()
|
||||
private val status = fakeStatusViewData()
|
||||
|
||||
private val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
on { statusData } doReturn mutableListOf(status)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ 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
|
||||
|
|
@ -88,9 +88,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
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",
|
||||
|
|
@ -114,9 +114,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) }
|
||||
|
||||
val newStatusData = mutableListOf(
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("6"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("6"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
verify(timelineViewModel).nextKey = "4"
|
||||
|
|
@ -129,9 +129,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
@ExperimentalPagingApi
|
||||
fun `should not prepend statuses`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
|
|
@ -139,9 +139,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
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")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -152,9 +152,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "0"
|
||||
|
|
@ -165,11 +165,11 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { 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)
|
||||
|
|
@ -181,9 +181,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
@ExperimentalPagingApi
|
||||
fun `should refresh and insert placeholder`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
|
|
@ -191,9 +191,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
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")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -204,9 +204,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("3"),
|
||||
mockStatusViewData("2"),
|
||||
mockStatusViewData("1")
|
||||
fakeStatusViewData("3"),
|
||||
fakeStatusViewData("2"),
|
||||
fakeStatusViewData("1")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "0"
|
||||
|
|
@ -217,12 +217,12 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { 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)
|
||||
|
|
@ -234,9 +234,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
@ExperimentalPagingApi
|
||||
fun `should refresh and not insert placeholders`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
|
|
@ -244,9 +244,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
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")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -257,9 +257,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "3"
|
||||
|
|
@ -270,12 +270,12 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { 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)
|
||||
|
|
@ -287,9 +287,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
@ExperimentalPagingApi
|
||||
fun `should append statuses`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
|
|
@ -297,9 +297,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
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",
|
||||
|
|
@ -314,9 +314,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = "3"
|
||||
|
|
@ -327,12 +327,12 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { 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)
|
||||
|
|
@ -344,9 +344,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
@ExperimentalPagingApi
|
||||
fun `should not append statuses when pagination end has been reached`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
|
|
@ -360,9 +360,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
listOf(
|
||||
PagingSource.LoadResult.Page(
|
||||
data = listOf(
|
||||
mockStatusViewData("8"),
|
||||
mockStatusViewData("7"),
|
||||
mockStatusViewData("5")
|
||||
fakeStatusViewData("8"),
|
||||
fakeStatusViewData("7"),
|
||||
fakeStatusViewData("5")
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = null
|
||||
|
|
@ -373,9 +373,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { 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)
|
||||
|
|
@ -387,9 +387,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
@ExperimentalPagingApi
|
||||
fun `should not append duplicates for trending statuses`() {
|
||||
val statuses: MutableList<StatusViewData> = mutableListOf(
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4"),
|
||||
mockStatusViewData("3")
|
||||
fakeStatusViewData("5"),
|
||||
fakeStatusViewData("4"),
|
||||
fakeStatusViewData("3")
|
||||
)
|
||||
|
||||
val timelineViewModel: NetworkTimelineViewModel = mock {
|
||||
|
|
@ -398,9 +398,9 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
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",
|
||||
|
|
@ -424,11 +424,11 @@ class NetworkTimelineRemoteMediatorTest {
|
|||
val result = runBlocking { 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,189 @@
|
|||
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"
|
||||
) = Status(
|
||||
id = id,
|
||||
url = "https://$domain/@ConnyDuck/$id",
|
||||
account = fakeAccount(
|
||||
id = authorServerId,
|
||||
domain = domain
|
||||
),
|
||||
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 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,
|
||||
)
|
||||
},
|
||||
loading = false
|
||||
)
|
||||
}
|
||||
|
||||
fun fakePlaceholderHomeTimelineData(
|
||||
id: String
|
||||
) = HomeTimelineData(
|
||||
id = id,
|
||||
account = null,
|
||||
status = null,
|
||||
reblogAccount = 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,12 @@ 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.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
|
||||
|
|
@ -118,9 +118,9 @@ class ViewThreadViewModelTest {
|
|||
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")
|
||||
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")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
|
|
@ -133,7 +133,7 @@ class ViewThreadViewModelTest {
|
|||
@Test
|
||||
fun `should emit status even if context fails to load`() {
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ class ViewThreadViewModelTest {
|
|||
assertEquals(
|
||||
ThreadUiState.Success(
|
||||
statusViewData = listOf(
|
||||
mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true)
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true)
|
||||
),
|
||||
detailedStatusPosition = 0,
|
||||
revealButton = RevealButtonState.NO_BUTTON
|
||||
|
|
@ -176,8 +176,8 @@ class ViewThreadViewModelTest {
|
|||
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"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -203,9 +203,9 @@ class ViewThreadViewModelTest {
|
|||
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)
|
||||
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)
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.HIDE
|
||||
|
|
@ -222,14 +222,14 @@ class ViewThreadViewModelTest {
|
|||
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")
|
||||
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")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
|
|
@ -245,14 +245,14 @@ class ViewThreadViewModelTest {
|
|||
|
||||
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")
|
||||
fakeStatusViewData(id = "1", spoilerText = "Test"),
|
||||
fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
|
|
@ -270,16 +270,16 @@ class ViewThreadViewModelTest {
|
|||
|
||||
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")
|
||||
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")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
|
|
@ -297,16 +297,16 @@ class ViewThreadViewModelTest {
|
|||
|
||||
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")
|
||||
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")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
|
|
@ -324,16 +324,16 @@ class ViewThreadViewModelTest {
|
|||
|
||||
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")
|
||||
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")
|
||||
),
|
||||
detailedStatusPosition = 1,
|
||||
revealButton = RevealButtonState.REVEAL
|
||||
|
|
@ -345,11 +345,11 @@ class ViewThreadViewModelTest {
|
|||
|
||||
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"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [28])
|
||||
@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,234 @@
|
|||
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 = [28])
|
||||
@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.SIGN_UP, 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")),
|
||||
)
|
||||
|
||||
db.insert(notificationsAccount1, tuskyAccountId = 1)
|
||||
db.insert(listOf(fakeNotification(id = "6")), 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("3", "4", "5", "6")
|
||||
|
||||
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 = [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() = 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,7 +1,7 @@
|
|||
package com.keylesspalace.tusky.network
|
||||
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ 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 com.keylesspalace.tusky.db.entity.AccountEntity
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue