android: add message core tests and update checklist docs
Some checks failed
CI / test (push) Failing after 2m14s

This commit is contained in:
Codex
2026-03-09 02:13:20 +03:00
parent 545b45c5db
commit 4fa657ff7a
5 changed files with 243 additions and 4 deletions

View File

@@ -91,3 +91,9 @@
- Added input composer with send flow, reply/edit modes, and inline action cancellation. - Added input composer with send flow, reply/edit modes, and inline action cancellation.
- Added long-press actions (`reply`, `edit`, `delete`) for baseline message operations. - Added long-press actions (`reply`, `edit`, `delete`) for baseline message operations.
- Added manual "load older" pagination trigger and chat back navigation integration. - Added manual "load older" pagination trigger and chat back navigation integration.
### Step 15 - Sprint A / 5) Message tests and docs
- Added unit tests for `NetworkMessageRepository` sync/send flows.
- Added DAO test for message scoped replace behavior in Room.
- Expanded realtime parser tests with rich `receive_message` mapping coverage.
- Updated `docs/android-checklist.md` for completed message-core items.

View File

@@ -0,0 +1,79 @@
package ru.daemonlord.messenger.data.message.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.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.daemonlord.messenger.data.chat.local.db.MessengerDatabase
import ru.daemonlord.messenger.data.message.local.entity.MessageEntity
@RunWith(RobolectricTestRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class MessageDaoTest {
private lateinit var db: MessengerDatabase
private lateinit var dao: MessageDao
@Before
fun setUp() {
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
MessengerDatabase::class.java,
).allowMainThreadQueries()
.build()
dao = db.messageDao()
}
@After
fun tearDown() {
db.close()
}
@Test
fun clearAndReplaceMessages_replacesOnlyTargetChatMessages() = runTest {
dao.upsertMessages(
listOf(
message(id = 1, chatId = 10, text = "old-10"),
message(id = 2, chatId = 20, text = "keep-20"),
)
)
dao.clearAndReplaceMessages(
chatId = 10,
messages = listOf(message(id = 3, chatId = 10, text = "new-10")),
attachments = emptyList(),
)
val chat10 = dao.observeRecentMessages(chatId = 10).first()
val chat20 = dao.observeRecentMessages(chatId = 20).first()
assertEquals(1, chat10.size)
assertEquals(3L, chat10.first().id)
assertEquals(1, chat20.size)
assertEquals(2L, chat20.first().id)
}
private fun message(id: Long, chatId: Long, text: String): MessageEntity {
return MessageEntity(
id = id,
chatId = chatId,
senderId = 1,
senderDisplayName = "User",
senderUsername = "user",
senderAvatarUrl = null,
replyToMessageId = null,
type = "text",
text = text,
status = null,
createdAt = "2026-03-08T12:00:00Z",
updatedAt = null,
)
}
}

View File

@@ -0,0 +1,124 @@
package ru.daemonlord.messenger.data.message.repository
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.daemonlord.messenger.data.chat.local.db.MessengerDatabase
import ru.daemonlord.messenger.data.message.api.MessageApiService
import ru.daemonlord.messenger.data.message.dto.MessageCreateRequestDto
import ru.daemonlord.messenger.data.message.dto.MessageReadDto
import ru.daemonlord.messenger.data.message.dto.MessageUpdateRequestDto
@RunWith(RobolectricTestRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class NetworkMessageRepositoryTest {
private lateinit var db: MessengerDatabase
private lateinit var fakeApi: FakeMessageApiService
private val dispatcher = StandardTestDispatcher()
@Before
fun setUp() {
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
MessengerDatabase::class.java,
).allowMainThreadQueries().build()
fakeApi = FakeMessageApiService()
}
@After
fun tearDown() {
db.close()
}
@Test
fun syncRecentMessages_writesRemoteItemsToRoom() = runTest(dispatcher) {
fakeApi.messages = listOf(
MessageReadDto(
id = 101,
chatId = 5,
senderId = 7,
type = "text",
text = "hello",
createdAt = "2026-03-08T10:00:00Z",
updatedAt = null,
)
)
val repository = createRepository()
val sync = repository.syncRecentMessages(chatId = 5)
assertTrue(sync is ru.daemonlord.messenger.domain.common.AppResult.Success)
val items = repository.observeMessages(chatId = 5).first()
assertEquals(1, items.size)
assertEquals(101L, items.first().id)
assertEquals("hello", items.first().text)
}
@Test
fun sendTextMessage_optimisticThenServerReplace() = runTest(dispatcher) {
fakeApi.sendResponse = MessageReadDto(
id = 202,
chatId = 8,
senderId = 1,
type = "text",
text = "from server",
createdAt = "2026-03-08T11:00:00Z",
updatedAt = null,
)
val repository = createRepository()
val result = repository.sendTextMessage(chatId = 8, text = "from client")
assertTrue(result is ru.daemonlord.messenger.domain.common.AppResult.Success)
val items = repository.observeMessages(chatId = 8).first()
assertEquals(1, items.size)
assertEquals(202L, items.first().id)
assertEquals("from server", items.first().text)
}
private fun createRepository(): NetworkMessageRepository {
return NetworkMessageRepository(
messageApiService = fakeApi,
messageDao = db.messageDao(),
chatDao = db.chatDao(),
ioDispatcher = dispatcher,
)
}
private class FakeMessageApiService : MessageApiService {
var messages: List<MessageReadDto> = emptyList()
var sendResponse: MessageReadDto = MessageReadDto(
id = 1,
chatId = 1,
senderId = 1,
type = "text",
text = "ok",
createdAt = "2026-03-08T10:00:00Z",
)
override suspend fun getMessages(chatId: Long, limit: Int, beforeId: Long?): List<MessageReadDto> {
return messages
}
override suspend fun sendMessage(request: MessageCreateRequestDto): MessageReadDto {
return sendResponse
}
override suspend fun editMessage(messageId: Long, request: MessageUpdateRequestDto): MessageReadDto {
return sendResponse.copy(id = messageId, text = request.text)
}
override suspend fun deleteMessage(messageId: Long, forAll: Boolean) = Unit
}
}

View File

@@ -41,4 +41,34 @@ class RealtimeEventParserTest {
assertTrue(event is RealtimeEvent.ChatDeleted) assertTrue(event is RealtimeEvent.ChatDeleted)
assertEquals(77L, (event as RealtimeEvent.ChatDeleted).chatId) assertEquals(77L, (event as RealtimeEvent.ChatDeleted).chatId)
} }
@Test
fun parseReceiveMessage_returnsRichMappedEvent() {
val payload = """
{
"event": "receive_message",
"payload": {
"chat_id": 88,
"message": {
"id": 9001,
"chat_id": 88,
"sender_id": 5,
"reply_to_message_id": 100,
"type": "text",
"text": "hi",
"created_at": "2026-03-08T12:00:00Z"
}
}
}
""".trimIndent()
val event = parser.parse(payload)
assertTrue(event is RealtimeEvent.ReceiveMessage)
val mapped = event as RealtimeEvent.ReceiveMessage
assertEquals(88L, mapped.chatId)
assertEquals(9001L, mapped.messageId)
assertEquals(5L, mapped.senderId)
assertEquals(100L, mapped.replyToMessageId)
assertEquals("hi", mapped.text)
}
} }

View File

@@ -49,10 +49,10 @@
- [x] Online indicator в private чатах - [x] Online indicator в private чатах
## 7. Сообщения ## 7. Сообщения
- [ ] Отправка текста - [x] Отправка текста
- [ ] Reply/quote - [x] Reply/quote
- [ ] Edit (<=7 дней) - [x] Edit (<=7 дней)
- [ ] Delete for me / for all (по правам) - [x] Delete for me / for all (по правам)
- [ ] Forward в 1+ чатов - [ ] Forward в 1+ чатов
- [ ] Reactions - [ ] Reactions
- [ ] Delivery/read states - [ ] Delivery/read states