android: add message core tests and update checklist docs
Some checks failed
CI / test (push) Failing after 2m14s
Some checks failed
CI / test (push) Failing after 2m14s
This commit is contained in:
@@ -91,3 +91,9 @@
|
||||
- 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 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.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,34 @@ class RealtimeEventParserTest {
|
||||
assertTrue(event is RealtimeEvent.ChatDeleted)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
- [x] Online indicator в private чатах
|
||||
|
||||
## 7. Сообщения
|
||||
- [ ] Отправка текста
|
||||
- [ ] Reply/quote
|
||||
- [ ] Edit (<=7 дней)
|
||||
- [ ] Delete for me / for all (по правам)
|
||||
- [x] Отправка текста
|
||||
- [x] Reply/quote
|
||||
- [x] Edit (<=7 дней)
|
||||
- [x] Delete for me / for all (по правам)
|
||||
- [ ] Forward в 1+ чатов
|
||||
- [ ] Reactions
|
||||
- [ ] Delivery/read states
|
||||
|
||||
Reference in New Issue
Block a user