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 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.
|
||||||
|
|||||||
@@ -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)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user