fix(dm): WebSocket heartbeat와 token 재연결을 보정한다
This commit is contained in:
@@ -55,6 +55,7 @@ class DmChatRoomViewModelTest {
|
||||
private lateinit var socketClient: DmChatSocketClient
|
||||
private lateinit var reconnectScheduler: TestScheduler
|
||||
private lateinit var viewModel: DmChatRoomViewModel
|
||||
private var token: String = "test-token"
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -62,6 +63,7 @@ class DmChatRoomViewModelTest {
|
||||
SharedPreferenceManager.resetForTest()
|
||||
SharedPreferenceManager.init(context)
|
||||
SharedPreferenceManager.token = "test-token"
|
||||
token = "test-token"
|
||||
api = FakeDmChatApi()
|
||||
socketFactory = FakeWebSocketFactory()
|
||||
socketClient = DmChatSocketClient(
|
||||
@@ -74,7 +76,7 @@ class DmChatRoomViewModelTest {
|
||||
viewModel = DmChatRoomViewModel(
|
||||
repository = DmChatRepository(api, socketClient),
|
||||
reconnectScheduler = reconnectScheduler,
|
||||
tokenProvider = { "test-token" }
|
||||
tokenProvider = { token }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -651,7 +653,11 @@ class DmChatRoomViewModelTest {
|
||||
).readText()
|
||||
val compactSource = source.filterNot { it.isWhitespace() }
|
||||
|
||||
assertTrue(compactSource.contains("scheduleRealtimeCallback{if(shouldReconnectRealtime)connectRealtime(token=token)}"))
|
||||
assertTrue(
|
||||
compactSource.contains(
|
||||
"scheduleRealtimeCallback{if(shouldReconnectRealtime)connectRealtime(token=authToken().ifBlank{token})}"
|
||||
)
|
||||
)
|
||||
assertTrue(!compactSource.contains("scheduleDirect({connectRealtime(token=token)}"))
|
||||
}
|
||||
|
||||
@@ -714,6 +720,109 @@ class DmChatRoomViewModelTest {
|
||||
assertEquals(2, socketFactory.connectCalls.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leave는 LEAVE_ROOM 전송 후 socket을 close하고 중복 호출은 무시한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
|
||||
viewModel.leaveRealtime()
|
||||
viewModel.leaveRealtime()
|
||||
|
||||
assertEquals(listOf("JOIN_ROOM", "LEAVE_ROOM"), socketFactory.webSocket.sentTexts.map { it.type() })
|
||||
assertEquals(1, socketFactory.closeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JOINED 이후 heartbeat는 PING을 보내고 PONG 수신 시 연결을 유지한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
api.enqueueMessagesSuccess(messagesPage(messages = emptyList()))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
socketFactory.emitJoined()
|
||||
|
||||
reconnectScheduler.advanceTimeBy(30L, TimeUnit.SECONDS)
|
||||
reconnectScheduler.advanceTimeBy(5L, TimeUnit.SECONDS)
|
||||
socketFactory.emitPong()
|
||||
reconnectScheduler.advanceTimeBy(24L, TimeUnit.SECONDS)
|
||||
|
||||
assertEquals(listOf("JOIN_ROOM", "PING"), socketFactory.webSocket.sentTexts.map { it.type() })
|
||||
assertEquals(true, viewModel.isRealtimeConnectedForTest())
|
||||
assertEquals(0, socketFactory.closeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `heartbeat PONG timeout은 socket close 후 foreground 조건에서 reconnect를 예약한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
api.enqueueMessagesSuccess(messagesPage(messages = emptyList()))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
socketFactory.emitJoined()
|
||||
|
||||
reconnectScheduler.advanceTimeBy(30L, TimeUnit.SECONDS)
|
||||
reconnectScheduler.advanceTimeBy(10L, TimeUnit.SECONDS)
|
||||
reconnectScheduler.advanceTimeBy(2999L, TimeUnit.MILLISECONDS)
|
||||
|
||||
assertEquals(false, viewModel.isRealtimeConnectedForTest())
|
||||
assertEquals(1, socketFactory.closeCount)
|
||||
assertEquals(1, socketFactory.connectCalls.size)
|
||||
|
||||
reconnectScheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS)
|
||||
|
||||
assertEquals(2, socketFactory.connectCalls.size)
|
||||
assertEquals("JOIN_ROOM", socketFactory.webSocket.sentTexts.lastJson().get("type").asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leave는 heartbeat timeout과 reconnect 예약을 취소한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
api.enqueueMessagesSuccess(messagesPage(messages = emptyList()))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
socketFactory.emitJoined()
|
||||
|
||||
reconnectScheduler.advanceTimeBy(30L, TimeUnit.SECONDS)
|
||||
viewModel.leaveRealtime()
|
||||
reconnectScheduler.advanceTimeBy(13L, TimeUnit.SECONDS)
|
||||
|
||||
assertEquals(listOf(RealtimeConnectCall("test-token", 10L)), socketFactory.connectCalls)
|
||||
assertEquals(listOf("JOIN_ROOM", "PING", "LEAVE_ROOM"), socketFactory.webSocket.sentTexts.map { it.type() })
|
||||
assertEquals(1, socketFactory.closeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `token이 변경되면 기존 socket을 close하고 새 token으로 다시 JOIN_ROOM을 보낸다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
api.enqueueMessagesSuccess(messagesPage(messages = emptyList()))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
socketFactory.emitJoined()
|
||||
|
||||
token = "new-token"
|
||||
viewModel.connectRealtime()
|
||||
|
||||
assertEquals(
|
||||
listOf(RealtimeConnectCall("test-token", 10L), RealtimeConnectCall("new-token", 10L)),
|
||||
socketFactory.connectCalls
|
||||
)
|
||||
assertEquals(1, socketFactory.closeCount)
|
||||
assertEquals("JOIN_ROOM", socketFactory.webSocket.sentTexts.lastJson().get("type").asString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leave 이후 token이 변경되어도 socket reconnect를 진행하지 않는다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
viewModel.leaveRealtime()
|
||||
|
||||
token = "new-token"
|
||||
reconnectScheduler.advanceTimeBy(30L, TimeUnit.SECONDS)
|
||||
|
||||
assertEquals(listOf(RealtimeConnectCall("test-token", 10L)), socketFactory.connectCalls)
|
||||
assertEquals(1, socketFactory.closeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `realtime leave는 채팅 상태를 Error로 바꾸지 않는다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L, messages = listOf(message(messageId = 1L, textMessage = "기존"))))
|
||||
@@ -972,6 +1081,10 @@ class FakeWebSocketFactory {
|
||||
)
|
||||
}
|
||||
|
||||
fun emitPong() {
|
||||
webSocketListener?.onMessage(webSocket, "{\"type\":\"PONG\",\"payload\":{}}")
|
||||
}
|
||||
|
||||
fun emitFailure(throwable: Throwable) {
|
||||
webSocketListener?.onFailure(webSocket, throwable, null)
|
||||
}
|
||||
@@ -997,3 +1110,7 @@ class FakeWebSocket : WebSocket {
|
||||
|
||||
fun sentJsonAt(index: Int) = JsonParser.parseString(sentTexts[index]).asJsonObject
|
||||
}
|
||||
|
||||
private fun String.type(): String = JsonParser.parseString(this).asJsonObject.get("type").asString
|
||||
|
||||
private fun List<String>.lastJson() = JsonParser.parseString(last()).asJsonObject
|
||||
|
||||
Reference in New Issue
Block a user