545 lines
20 KiB
Kotlin
545 lines
20 KiB
Kotlin
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(
|
|
mockNotification(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(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(id = "1")
|
|
)
|
|
|
|
db.insert(notificationsAlreadyInDb)
|
|
|
|
val remoteMediator = NotificationsRemoteMediator(
|
|
accountManager = accountManager,
|
|
api = mock {
|
|
onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "8"),
|
|
mockNotification(id = "7"),
|
|
mockNotification(id = "5")
|
|
)
|
|
)
|
|
onBlocking { notifications(maxId = "3", limit = 3, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(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(
|
|
mockNotification(id = "8").toNotificationDataEntity(1),
|
|
mockNotification(id = "7").toNotificationDataEntity(1),
|
|
Placeholder(id = "5", loading = false).toNotificationDataEntity(1),
|
|
mockNotification(id = "3").toNotificationDataEntity(1),
|
|
mockNotification(id = "2").toNotificationDataEntity(1),
|
|
mockNotification(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(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(id = "1")
|
|
)
|
|
|
|
db.insert(notificationsAlreadyInDb)
|
|
|
|
val remoteMediator = NotificationsRemoteMediator(
|
|
accountManager = accountManager,
|
|
api = mock {
|
|
onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "8"),
|
|
mockNotification(id = "7"),
|
|
mockNotification(id = "5")
|
|
)
|
|
)
|
|
onBlocking { notifications(maxId = "3", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(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(
|
|
mockNotification(id = "8").toNotificationDataEntity(1),
|
|
mockNotification(id = "7").toNotificationDataEntity(1),
|
|
mockNotification(id = "5").toNotificationDataEntity(1),
|
|
mockNotification(id = "3").toNotificationDataEntity(1),
|
|
mockNotification(id = "2").toNotificationDataEntity(1),
|
|
mockNotification(id = "1").toNotificationDataEntity(1)
|
|
)
|
|
)
|
|
}
|
|
|
|
@Test
|
|
@ExperimentalPagingApi
|
|
fun `should refresh and not insert placeholders when there is overlap with existing notifications`() = runTest {
|
|
val notificationsAlreadyInDb = listOf(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(id = "1")
|
|
)
|
|
|
|
db.insert(notificationsAlreadyInDb)
|
|
|
|
val remoteMediator = NotificationsRemoteMediator(
|
|
accountManager = accountManager,
|
|
api = mock {
|
|
onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "6"),
|
|
mockNotification(id = "4"),
|
|
mockNotification(id = "3")
|
|
)
|
|
)
|
|
onBlocking { notifications(maxId = "3", limit = 3, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(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(
|
|
mockNotification(id = "6").toNotificationDataEntity(1),
|
|
mockNotification(id = "4").toNotificationDataEntity(1),
|
|
mockNotification(id = "3").toNotificationDataEntity(1),
|
|
mockNotification(id = "2").toNotificationDataEntity(1),
|
|
mockNotification(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(
|
|
mockNotification(id = "5"),
|
|
mockNotification(id = "4"),
|
|
mockNotification(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(
|
|
mockNotification(id = "5").toNotificationDataEntity(1),
|
|
mockNotification(id = "4").toNotificationDataEntity(1),
|
|
mockNotification(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(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(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(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "1")
|
|
)
|
|
)
|
|
},
|
|
db = db,
|
|
excludes = emptySet()
|
|
)
|
|
|
|
val state = state(
|
|
listOf(
|
|
PagingSource.LoadResult.Page(
|
|
data = listOf(
|
|
mockNotification(id = "3").toNotificationDataEntity(1, isStatusExpanded = true),
|
|
mockNotification(id = "2").toNotificationDataEntity(1, isStatusExpanded = true),
|
|
mockNotification(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(
|
|
mockNotification(id = "3").toNotificationDataEntity(1, isStatusExpanded = true),
|
|
mockNotification(id = "1").toNotificationDataEntity(1, isStatusContentShowing = true)
|
|
)
|
|
)
|
|
}
|
|
|
|
@Test
|
|
@ExperimentalPagingApi
|
|
fun `should not remove placeholder in timeline`() = runTest {
|
|
val notificationsAlreadyInDb = listOf(
|
|
mockNotification(id = "8"),
|
|
mockNotification(id = "7"),
|
|
mockNotification(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(
|
|
mockNotification(id = "9"),
|
|
mockNotification(id = "8"),
|
|
mockNotification(id = "7")
|
|
)
|
|
)
|
|
onBlocking { notifications(maxId = "8", sinceId = "6", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "8"),
|
|
mockNotification(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(
|
|
mockNotification(id = "9").toNotificationDataEntity(1),
|
|
mockNotification(id = "8").toNotificationDataEntity(1),
|
|
mockNotification(id = "7").toNotificationDataEntity(1),
|
|
Placeholder(id = "6", loading = false).toNotificationDataEntity(1),
|
|
mockNotification(id = "1").toNotificationDataEntity(1)
|
|
)
|
|
)
|
|
}
|
|
|
|
@Test
|
|
@ExperimentalPagingApi
|
|
fun `should append notifications`() = runTest {
|
|
val notificationsAlreadyInDb = listOf(
|
|
mockNotification(id = "8"),
|
|
mockNotification(id = "7"),
|
|
mockNotification(id = "5")
|
|
)
|
|
|
|
db.insert(notificationsAlreadyInDb)
|
|
|
|
val remoteMediator = NotificationsRemoteMediator(
|
|
accountManager = accountManager,
|
|
api = mock {
|
|
onBlocking { notifications(maxId = "5", limit = 20, excludes = emptySet()) } doReturn Response.success(
|
|
listOf(
|
|
mockNotification(id = "3"),
|
|
mockNotification(id = "2"),
|
|
mockNotification(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(
|
|
mockNotification(id = "8").toNotificationDataEntity(1),
|
|
mockNotification(id = "7").toNotificationDataEntity(1),
|
|
mockNotification(id = "5").toNotificationDataEntity(1),
|
|
mockNotification(id = "3").toNotificationDataEntity(1),
|
|
mockNotification(id = "2").toNotificationDataEntity(1),
|
|
mockNotification(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
|
|
)
|
|
}
|