android: add chat/realtime tests and update android checklist

This commit is contained in:
Codex
2026-03-08 22:34:41 +03:00
parent 2dfad1a624
commit 9d842c1d88
6 changed files with 350 additions and 16 deletions

View File

@@ -0,0 +1,94 @@
package ru.daemonlord.messenger.data.chat.local.dao
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.robolectric.RobolectricTestRunner
import org.junit.runner.RunWith
import ru.daemonlord.messenger.data.chat.local.db.MessengerDatabase
import ru.daemonlord.messenger.data.chat.local.entity.ChatEntity
import ru.daemonlord.messenger.data.chat.local.entity.UserShortEntity
@RunWith(RobolectricTestRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class ChatDaoTest {
private lateinit var db: MessengerDatabase
private lateinit var dao: ChatDao
@Before
fun setUp() {
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
MessengerDatabase::class.java,
).allowMainThreadQueries()
.build()
dao = db.chatDao()
}
@After
fun tearDown() {
db.close()
}
@Test
fun clearAndReplaceChats_replacesOnlySelectedArchiveScope() = runTest {
dao.upsertChats(
listOf(
chatEntity(id = 1, archived = false, pinned = false, title = "All chat"),
chatEntity(id = 2, archived = true, pinned = true, title = "Archived chat"),
)
)
dao.clearAndReplaceChats(
archived = false,
chats = listOf(chatEntity(id = 3, archived = false, pinned = false, title = "New all chat")),
users = listOf(UserShortEntity(id = 99, displayName = "User", username = "user", avatarUrl = null)),
)
val allChats = dao.observeChats(archived = false).first()
val archivedChats = dao.observeChats(archived = true).first()
assertEquals(1, allChats.size)
assertEquals(3L, allChats.first().id)
assertEquals(1, archivedChats.size)
assertEquals(2L, archivedChats.first().id)
}
private fun chatEntity(
id: Long,
archived: Boolean,
pinned: Boolean,
title: String,
): ChatEntity {
return ChatEntity(
id = id,
publicId = "public-$id",
type = "private",
title = title,
displayTitle = title,
handle = null,
avatarUrl = null,
archived = archived,
pinned = pinned,
muted = false,
unreadCount = 0,
unreadMentionsCount = 0,
counterpartUserId = null,
counterpartName = null,
counterpartUsername = null,
counterpartAvatarUrl = null,
counterpartIsOnline = false,
counterpartLastSeenAt = null,
lastMessageText = "hi",
lastMessageType = "text",
lastMessageCreatedAt = "2026-03-08T10:00:00Z",
updatedSortAt = "2026-03-08T10:00:00Z",
)
}
}

View File

@@ -0,0 +1,187 @@
package ru.daemonlord.messenger.data.chat.repository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import ru.daemonlord.messenger.data.chat.api.ChatApiService
import ru.daemonlord.messenger.data.chat.dto.ChatReadDto
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
import ru.daemonlord.messenger.data.chat.local.entity.ChatEntity
import ru.daemonlord.messenger.data.chat.local.entity.UserShortEntity
import ru.daemonlord.messenger.data.chat.local.model.ChatListLocalModel
@OptIn(ExperimentalCoroutinesApi::class)
class NetworkChatRepositoryTest {
private val dispatcher = StandardTestDispatcher()
@Test
fun observeChats_cacheFirstThenNetworkSync() = runTest(dispatcher) {
val cached = ChatEntity(
id = 1,
publicId = "cached-public",
type = "private",
title = null,
displayTitle = "Cached chat",
handle = null,
avatarUrl = null,
archived = false,
pinned = false,
muted = false,
unreadCount = 0,
unreadMentionsCount = 0,
counterpartUserId = null,
counterpartName = null,
counterpartUsername = null,
counterpartAvatarUrl = null,
counterpartIsOnline = false,
counterpartLastSeenAt = null,
lastMessageText = "Cached message",
lastMessageType = "text",
lastMessageCreatedAt = "2026-03-08T10:00:00Z",
updatedSortAt = "2026-03-08T10:00:00Z",
)
val fakeDao = FakeChatDao(initialChats = listOf(cached))
val fakeApi = FakeChatApiService(
chats = listOf(
ChatReadDto(
id = 2,
publicId = "remote-public",
type = "private",
displayTitle = "Remote chat",
archived = false,
pinned = true,
muted = false,
unreadCount = 5,
unreadMentionsCount = 1,
lastMessageText = "Remote message",
lastMessageType = "text",
lastMessageCreatedAt = "2026-03-08T11:00:00Z",
createdAt = "2026-03-08T09:00:00Z",
)
)
)
val repository = NetworkChatRepository(
chatApiService = fakeApi,
chatDao = fakeDao,
ioDispatcher = dispatcher,
)
val emissions = mutableListOf<List<String>>()
val collectJob = backgroundScope.launch(dispatcher) {
repository.observeChats(archived = false)
.take(2)
.toList()
.forEach { list -> emissions.add(list.map { it.displayTitle }) }
}
advanceUntilIdle()
collectJob.join()
assertEquals(listOf("Cached chat"), emissions[0])
assertEquals(listOf("Remote chat"), emissions[1])
assertTrue(fakeApi.requestedArchivedParams.contains(false))
}
private class FakeChatApiService(
private val chats: List<ChatReadDto>,
) : ChatApiService {
val requestedArchivedParams = mutableListOf<Boolean>()
override suspend fun getChats(archived: Boolean): List<ChatReadDto> {
requestedArchivedParams += archived
return chats
}
override suspend fun getChatById(chatId: Long): ChatReadDto {
return chats.first()
}
}
private class FakeChatDao(
initialChats: List<ChatEntity>,
) : ChatDao {
private val chats = MutableStateFlow(initialChats)
override fun observeChats(archived: Boolean): Flow<List<ChatListLocalModel>> {
return chats.map { entities ->
entities.filter { it.archived == archived }.map { it.toLocalModel() }
}
}
override suspend fun upsertChats(chats: List<ChatEntity>) {
val merged = this.chats.value.associateBy { it.id }.toMutableMap()
chats.forEach { merged[it.id] = it }
this.chats.value = merged.values.toList()
}
override suspend fun upsertUsers(users: List<UserShortEntity>) = Unit
override suspend fun deleteChatsByArchived(archived: Boolean) {
chats.value = chats.value.filterNot { it.archived == archived }
}
override suspend fun deleteChat(chatId: Long) {
chats.value = chats.value.filterNot { it.id == chatId }
}
override suspend fun updatePresence(chatId: Long, isOnline: Boolean, lastSeenAt: String?) = Unit
override suspend fun updateLastMessage(
chatId: Long,
lastMessageText: String?,
lastMessageType: String?,
lastMessageCreatedAt: String?,
updatedSortAt: String?,
) = Unit
override suspend fun incrementUnread(chatId: Long, incrementBy: Int) = Unit
override suspend fun clearAndReplaceChats(
archived: Boolean,
chats: List<ChatEntity>,
users: List<UserShortEntity>,
) {
deleteChatsByArchived(archived = archived)
upsertChats(chats)
}
private fun ChatEntity.toLocalModel(): ChatListLocalModel {
return ChatListLocalModel(
id = id,
publicId = publicId,
type = type,
title = title,
displayTitle = displayTitle,
handle = handle,
avatarUrl = avatarUrl,
archived = archived,
pinned = pinned,
muted = muted,
unreadCount = unreadCount,
unreadMentionsCount = unreadMentionsCount,
counterpartName = counterpartName,
counterpartUsername = counterpartUsername,
counterpartAvatarUrl = counterpartAvatarUrl,
counterpartIsOnline = counterpartIsOnline,
counterpartLastSeenAt = counterpartLastSeenAt,
lastMessageText = lastMessageText,
lastMessageType = lastMessageType,
lastMessageCreatedAt = lastMessageCreatedAt,
updatedSortAt = updatedSortAt,
)
}
}
}

View File

@@ -0,0 +1,44 @@
package ru.daemonlord.messenger.data.realtime
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import ru.daemonlord.messenger.domain.realtime.model.RealtimeEvent
class RealtimeEventParserTest {
private val parser = RealtimeEventParser(
json = Json { ignoreUnknownKeys = true }
)
@Test
fun parseChatUpdated_returnsMappedEvent() {
val payload = """
{
"event": "chat_updated",
"payload": { "chat_id": 42 },
"timestamp": "2026-03-08T12:00:00Z"
}
""".trimIndent()
val event = parser.parse(payload)
assertTrue(event is RealtimeEvent.ChatUpdated)
assertEquals(42L, (event as RealtimeEvent.ChatUpdated).chatId)
}
@Test
fun parseChatDeleted_returnsMappedEvent() {
val payload = """
{
"event": "chat_deleted",
"payload": { "chat_id": 77 },
"timestamp": "2026-03-08T12:00:00Z"
}
""".trimIndent()
val event = parser.parse(payload)
assertTrue(event is RealtimeEvent.ChatDeleted)
assertEquals(77L, (event as RealtimeEvent.ChatDeleted).chatId)
}
}