test #426
@@ -17,10 +17,17 @@ class UserCreatorChatWebSocketHandler(
|
|||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) : TextWebSocketHandler() {
|
) : TextWebSocketHandler() {
|
||||||
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
|
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
|
||||||
val request = objectMapper.readValue(message.payload, UserCreatorChatWebSocketMessage::class.java)
|
val request = try {
|
||||||
|
objectMapper.readValue(message.payload, UserCreatorChatWebSocketMessage::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sendProtocolError(session)
|
||||||
|
return
|
||||||
|
}
|
||||||
when (request.type) {
|
when (request.type) {
|
||||||
UserCreatorChatWebSocketMessageType.JOIN_ROOM -> handleJoinRoom(session, request)
|
UserCreatorChatWebSocketMessageType.JOIN_ROOM -> handleJoinRoom(session, request)
|
||||||
UserCreatorChatWebSocketMessageType.SEND_TEXT -> handleSendText(session, request)
|
UserCreatorChatWebSocketMessageType.SEND_TEXT -> handleSendText(session, request)
|
||||||
|
UserCreatorChatWebSocketMessageType.LEAVE_ROOM -> handleLeaveRoom(session, request)
|
||||||
|
UserCreatorChatWebSocketMessageType.PING -> handlePing(session, request)
|
||||||
else -> sendError(session, request, "common.error.invalid_request")
|
else -> sendError(session, request, "common.error.invalid_request")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,9 +55,7 @@ class UserCreatorChatWebSocketHandler(
|
|||||||
|
|
||||||
private fun handleSendText(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
private fun handleSendText(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||||
try {
|
try {
|
||||||
if (session.attributes[JOINED_ROOM_ID_ATTRIBUTE] != request.roomId) {
|
requireJoinedRoom(session, request.roomId)
|
||||||
throw SodaException(messageKey = "chat.room.join_required")
|
|
||||||
}
|
|
||||||
val textMessage = request.payload["textMessage"]?.asText().orEmpty()
|
val textMessage = request.payload["textMessage"]?.asText().orEmpty()
|
||||||
val message = service.sendTextMessageByWebSocket(
|
val message = service.sendTextMessageByWebSocket(
|
||||||
memberId = memberId(session),
|
memberId = memberId(session),
|
||||||
@@ -63,6 +68,31 @@ class UserCreatorChatWebSocketHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLeaveRoom(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||||
|
try {
|
||||||
|
requireJoinedRoom(session, request.roomId)
|
||||||
|
clearJoinedRoom(session)
|
||||||
|
} catch (e: SodaException) {
|
||||||
|
sendError(session, request, e.messageKey ?: "common.error.invalid_request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePing(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||||
|
try {
|
||||||
|
requireJoinedRoom(session, request.roomId)
|
||||||
|
presenceService.refresh(roomId = request.roomId, memberId = memberId(session), sessionId = session.id)
|
||||||
|
sendEnvelope(
|
||||||
|
session,
|
||||||
|
UserCreatorChatWebSocketMessageType.PONG,
|
||||||
|
request.requestId,
|
||||||
|
request.roomId,
|
||||||
|
emptyMap<String, Any>()
|
||||||
|
)
|
||||||
|
} catch (e: SodaException) {
|
||||||
|
sendError(session, request, e.messageKey ?: "common.error.invalid_request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
|
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
|
||||||
clearJoinedRoom(session)
|
clearJoinedRoom(session)
|
||||||
}
|
}
|
||||||
@@ -80,6 +110,12 @@ class UserCreatorChatWebSocketHandler(
|
|||||||
sessionRegistry.remove(session.id)
|
sessionRegistry.remove(session.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requireJoinedRoom(session: WebSocketSession, roomId: Long) {
|
||||||
|
if (session.attributes[JOINED_ROOM_ID_ATTRIBUTE] != roomId) {
|
||||||
|
throw SodaException(messageKey = "chat.room.join_required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendError(session: WebSocketSession, request: UserCreatorChatWebSocketMessage, messageKey: String) {
|
private fun sendError(session: WebSocketSession, request: UserCreatorChatWebSocketMessage, messageKey: String) {
|
||||||
sendEnvelope(
|
sendEnvelope(
|
||||||
session,
|
session,
|
||||||
@@ -90,6 +126,16 @@ class UserCreatorChatWebSocketHandler(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendProtocolError(session: WebSocketSession) {
|
||||||
|
sendEnvelope(
|
||||||
|
session,
|
||||||
|
UserCreatorChatWebSocketMessageType.ERROR,
|
||||||
|
null,
|
||||||
|
0L,
|
||||||
|
mapOf("messageKey" to "common.error.invalid_request")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendEnvelope(
|
private fun sendEnvelope(
|
||||||
session: WebSocketSession,
|
session: WebSocketSession,
|
||||||
type: UserCreatorChatWebSocketMessageType,
|
type: UserCreatorChatWebSocketMessageType,
|
||||||
|
|||||||
@@ -133,6 +133,86 @@ class UserCreatorChatWebSocketHandlerTest {
|
|||||||
assertEquals(emptyList<WebSocketSession>(), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
assertEquals(emptyList<WebSocketSession>(), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("LEAVE_ROOM은 local session과 Redis presence를 제거한다")
|
||||||
|
fun shouldRemoveLocalSessionAndPresenceWhenLeaveRoomReceived() {
|
||||||
|
val session = session("session-1", memberId = 1L)
|
||||||
|
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||||
|
Mockito.clearInvocations(session, presenceService)
|
||||||
|
|
||||||
|
handler.handleMessage(session, textMessage("LEAVE_ROOM", "leave-1", 10L, "{}"))
|
||||||
|
|
||||||
|
Mockito.verify(presenceService).markLeft(roomId = 10L, memberId = 1L, sessionId = "session-1")
|
||||||
|
assertEquals(emptyList<WebSocketSession>(), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("LEAVE_ROOM은 joined room과 요청 roomId가 다르면 presence를 제거하지 않고 ERROR를 응답한다")
|
||||||
|
fun shouldRejectLeaveRoomWhenRequestRoomDoesNotMatchJoinedRoom() {
|
||||||
|
val session = session("session-1", memberId = 1L)
|
||||||
|
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||||
|
Mockito.clearInvocations(session, presenceService)
|
||||||
|
|
||||||
|
handler.handleMessage(session, textMessage("LEAVE_ROOM", "leave-1", 20L, "{}"))
|
||||||
|
|
||||||
|
Mockito.verify(presenceService, Mockito.never()).markLeft(
|
||||||
|
roomId = 10L,
|
||||||
|
memberId = 1L,
|
||||||
|
sessionId = "session-1"
|
||||||
|
)
|
||||||
|
assertEquals(listOf(session), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
||||||
|
val response = sentJson(session)
|
||||||
|
assertEquals("ERROR", response["type"].asText())
|
||||||
|
assertEquals("leave-1", response["requestId"].asText())
|
||||||
|
assertEquals(20L, response["roomId"].asLong())
|
||||||
|
assertEquals("chat.room.join_required", response["payload"]["messageKey"].asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("PING은 joined room의 presence TTL을 갱신하고 PONG을 응답한다")
|
||||||
|
fun shouldRefreshPresenceAndSendPongWhenPingReceived() {
|
||||||
|
val session = session("session-1", memberId = 1L)
|
||||||
|
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||||
|
Mockito.clearInvocations(session, presenceService)
|
||||||
|
|
||||||
|
handler.handleMessage(session, textMessage("PING", "ping-1", 10L, "{}"))
|
||||||
|
|
||||||
|
Mockito.verify(presenceService).refresh(roomId = 10L, memberId = 1L, sessionId = "session-1")
|
||||||
|
val response = sentJson(session)
|
||||||
|
assertEquals("PONG", response["type"].asText())
|
||||||
|
assertEquals("ping-1", response["requestId"].asText())
|
||||||
|
assertEquals(10L, response["roomId"].asLong())
|
||||||
|
assertTrue(response["payload"].isObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("잘못된 JSON 메시지는 handler 밖으로 예외를 전파하지 않고 ERROR를 응답한다")
|
||||||
|
fun shouldSendErrorWhenMessagePayloadIsMalformedJson() {
|
||||||
|
val session = session("session-1", memberId = 1L)
|
||||||
|
|
||||||
|
handler.handleMessage(session, TextMessage("{"))
|
||||||
|
|
||||||
|
val response = sentJson(session)
|
||||||
|
assertEquals("ERROR", response["type"].asText())
|
||||||
|
assertTrue(response["requestId"].isNull)
|
||||||
|
assertEquals(0L, response["roomId"].asLong())
|
||||||
|
assertEquals("common.error.invalid_request", response["payload"]["messageKey"].asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("알 수 없는 type 메시지는 handler 밖으로 예외를 전파하지 않고 ERROR를 응답한다")
|
||||||
|
fun shouldSendErrorWhenMessageTypeIsUnknown() {
|
||||||
|
val session = session("session-1", memberId = 1L)
|
||||||
|
|
||||||
|
handler.handleMessage(session, textMessage("UNKNOWN", "unknown-1", 10L, "{}"))
|
||||||
|
|
||||||
|
val response = sentJson(session)
|
||||||
|
assertEquals("ERROR", response["type"].asText())
|
||||||
|
assertTrue(response["requestId"].isNull)
|
||||||
|
assertEquals(0L, response["roomId"].asLong())
|
||||||
|
assertEquals("common.error.invalid_request", response["payload"]["messageKey"].asText())
|
||||||
|
}
|
||||||
|
|
||||||
private fun textMessage(type: String, requestId: String, roomId: Long, payload: String): TextMessage {
|
private fun textMessage(type: String, requestId: String, roomId: Long, payload: String): TextMessage {
|
||||||
return TextMessage(
|
return TextMessage(
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user