fix(dm): MESSAGE 선도착 ACK 처리를 보정한다
This commit is contained in:
@@ -388,6 +388,86 @@ class DmChatRoomViewModelTest {
|
||||
assertEquals(1, state.messages.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MESSAGE requestId가 SEND_ACK보다 먼저 오면 pending local item을 확정한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
viewModel.sendText("선도착")
|
||||
val pendingItem = (viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content)
|
||||
.messages.single()
|
||||
val requestId = pendingItem.requestId!!
|
||||
|
||||
socketFactory.emitMessage(requestId, message(messageId = 60L, mine = true, textMessage = "선도착"))
|
||||
socketFactory.emitAck(requestId, message(messageId = 60L, mine = true, textMessage = "선도착"))
|
||||
|
||||
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
assertEquals(listOf(60L), state.messages.map { it.messageId })
|
||||
assertEquals(listOf(pendingItem.localId), state.messages.map { it.localId })
|
||||
assertEquals(listOf(DmChatMessageStatus.SENT), state.messages.map { it.status })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MESSAGE requestId가 없으면 ACK 후도착 시 같은 messageId 중복을 남기지 않는다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
viewModel.sendText("중복 방지")
|
||||
val requestId = (viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content)
|
||||
.messages.single().requestId!!
|
||||
|
||||
socketFactory.emitMessage(message(messageId = 61L, mine = true, textMessage = "중복 방지"))
|
||||
socketFactory.emitAck(requestId, message(messageId = 61L, mine = true, textMessage = "중복 방지"))
|
||||
|
||||
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
assertEquals(listOf(61L), state.messages.map { it.messageId })
|
||||
assertEquals(1, state.messages.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timeout 실패 후 늦은 SEND_ACK가 오면 같은 local item을 성공으로 복구한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
viewModel.sendText("늦은 ACK")
|
||||
val pendingItem = (viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content)
|
||||
.messages.single()
|
||||
val requestId = pendingItem.requestId!!
|
||||
|
||||
reconnectScheduler.advanceTimeBy(10L, TimeUnit.SECONDS)
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
socketFactory.emitAck(requestId, message(messageId = 62L, mine = true, textMessage = "늦은 ACK"))
|
||||
|
||||
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
assertEquals(listOf(62L), state.messages.map { it.messageId })
|
||||
assertEquals(listOf(pendingItem.localId), state.messages.map { it.localId })
|
||||
assertEquals(listOf(DmChatMessageStatus.SENT), state.messages.map { it.status })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retry 성공 후 이전 timeout request ACK는 같은 local item을 덮어쓰지 않는다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
viewModel.sendText("재시도 ACK")
|
||||
val firstState = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
val failedItem = firstState.messages.single()
|
||||
val firstRequestId = failedItem.requestId!!
|
||||
|
||||
reconnectScheduler.advanceTimeBy(10L, TimeUnit.SECONDS)
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
viewModel.retry(failedItem.localId!!)
|
||||
val retryRequestId = (viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content)
|
||||
.messages.single().requestId!!
|
||||
socketFactory.emitAck(retryRequestId, message(messageId = 70L, mine = true, textMessage = "최신 ACK"))
|
||||
socketFactory.emitAck(firstRequestId, message(messageId = 69L, mine = true, textMessage = "이전 ACK"))
|
||||
|
||||
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
assertEquals(listOf(70L), state.messages.map { it.messageId })
|
||||
assertEquals(listOf("최신 ACK"), state.messages.map { it.textMessage })
|
||||
assertEquals(listOf(failedItem.localId), state.messages.map { it.localId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `재연결 후 최신 메시지 동기화는 getMessages 결과를 병합한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L, messages = listOf(message(messageId = 1L, textMessage = "기존"))))
|
||||
@@ -621,20 +701,6 @@ class DmChatRoomViewModelTest {
|
||||
assertTrue(socketFactory.closeCount >= 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `realtime leave 중 중복 요청은 close를 반복할 수 있다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
|
||||
viewModel.leaveRealtime()
|
||||
viewModel.leaveRealtime()
|
||||
|
||||
assertEquals(0, socketFactory.closeCount)
|
||||
viewModel.leaveRealtime()
|
||||
|
||||
assertEquals(0, socketFactory.closeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leave 후 다시 background로 가면 새 소켓도 close한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
@@ -883,6 +949,14 @@ class FakeWebSocketFactory {
|
||||
webSocketListener?.onMessage(webSocket, "{\"type\":\"MESSAGE\",\"payload\":{\"message\":$json}}")
|
||||
}
|
||||
|
||||
fun emitMessage(requestId: String, message: DmChatMessageResponse) {
|
||||
val json = Gson().toJson(message)
|
||||
webSocketListener?.onMessage(
|
||||
webSocket,
|
||||
"{\"type\":\"MESSAGE\",\"payload\":{\"requestId\":\"$requestId\",\"message\":$json}}"
|
||||
)
|
||||
}
|
||||
|
||||
fun emitAck(requestId: String, message: DmChatMessageResponse) {
|
||||
val json = Gson().toJson(message)
|
||||
webSocketListener?.onMessage(
|
||||
|
||||
@@ -42,6 +42,26 @@ class DmChatSocketParserTest {
|
||||
assertEquals("안녕하세요", message.textMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MESSAGE type은 nullable requestId를 보존한다`() {
|
||||
val event = parser.parse(
|
||||
"""
|
||||
{
|
||||
"type": "MESSAGE",
|
||||
"payload": {
|
||||
"requestId": "request-1",
|
||||
"message": ${messageJson(messageId = 12L, textMessage = "선도착")}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val messageEvent = event as? DmChatSocketEvent.Message
|
||||
requireNotNull(messageEvent)
|
||||
assertEquals("request-1", messageEvent.requestId)
|
||||
assertEquals(12L, messageEvent.message.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SEND_ACK type은 requestId와 서버 확정 메시지로 파싱된다`() {
|
||||
val event = parser.parse(
|
||||
|
||||
Reference in New Issue
Block a user