feat(user-creator-chat): WebSocket room handler를 구현한다
This commit is contained in:
@@ -26,6 +26,9 @@ import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatRoomOpenRe
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceService
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBroker
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageType
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.PageRequest
|
||||
@@ -43,6 +46,8 @@ class UserCreatorChatService(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val blockMemberRepository: BlockMemberRepository,
|
||||
private val realtimeService: UserCreatorChatRealtimeService,
|
||||
private val presenceService: UserCreatorChatPresenceService,
|
||||
private val roomMessageBroker: UserCreatorChatRoomMessageBroker,
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val s3Uploader: S3Uploader,
|
||||
@@ -114,17 +119,30 @@ class UserCreatorChatService(
|
||||
): SendUserCreatorChatMessageResponse {
|
||||
if (request.textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
|
||||
val context = resolveSendContext(member, roomId)
|
||||
val message = messageRepository.save(
|
||||
UserCreatorChatMessage(
|
||||
chatRoom = context.room,
|
||||
participant = context.senderParticipant,
|
||||
messageType = UserCreatorChatMessageType.TEXT,
|
||||
textMessage = request.textMessage
|
||||
)
|
||||
)
|
||||
val message = saveTextMessage(context, request.textMessage)
|
||||
return deliverMessage(message, member, context.opponentParticipant)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun sendTextMessageByWebSocket(memberId: Long, roomId: Long, textMessage: String): UserCreatorChatMessageItemDto {
|
||||
if (textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
|
||||
val senderParticipant = validateParticipant(roomId, memberId)
|
||||
val sender = senderParticipant.member
|
||||
val context = resolveSendContext(sender, roomId)
|
||||
val message = saveTextMessage(context, textMessage)
|
||||
val senderMessage = toMessageItemDto(message, sender)
|
||||
val opponent = context.opponentParticipant.member
|
||||
if (presenceService.hasPresence(roomId, opponent.id!!)) {
|
||||
val opponentMessage = toMessageItemDto(message, opponent)
|
||||
roomMessageBroker.publish(
|
||||
roomId = roomId,
|
||||
memberId = opponent.id!!,
|
||||
payload = websocketMessagePayload(UserCreatorChatWebSocketMessageType.MESSAGE, roomId, opponentMessage)
|
||||
)
|
||||
}
|
||||
return senderMessage
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun sendVoiceMessage(
|
||||
member: Member,
|
||||
@@ -162,6 +180,21 @@ class UserCreatorChatService(
|
||||
realtimeService.disconnect(roomId, member.id!!)
|
||||
}
|
||||
|
||||
fun validateParticipant(roomId: Long, memberId: Long): UserCreatorChatParticipant {
|
||||
return requireParticipant(roomId, memberId)
|
||||
}
|
||||
|
||||
private fun saveTextMessage(context: SendContext, textMessage: String): UserCreatorChatMessage {
|
||||
return messageRepository.save(
|
||||
UserCreatorChatMessage(
|
||||
chatRoom = context.room,
|
||||
participant = context.senderParticipant,
|
||||
messageType = UserCreatorChatMessageType.TEXT,
|
||||
textMessage = textMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveSendContext(member: Member, roomId: Long): SendContext {
|
||||
val room = findRoom(roomId)
|
||||
val senderParticipant = requireParticipant(roomId, member.id!!)
|
||||
@@ -208,6 +241,21 @@ class UserCreatorChatService(
|
||||
)
|
||||
}
|
||||
|
||||
private fun websocketMessagePayload(
|
||||
type: UserCreatorChatWebSocketMessageType,
|
||||
roomId: Long,
|
||||
payload: UserCreatorChatMessageItemDto
|
||||
): String {
|
||||
return objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"type" to type,
|
||||
"requestId" to null,
|
||||
"roomId" to roomId,
|
||||
"payload" to payload
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun validateRecipient(sender: Member, recipient: Member) {
|
||||
if (!recipient.isActive) throw SodaException(messageKey = "message.error.recipient_inactive")
|
||||
if (recipient.memberKind == MemberKind.AI_CHARACTER) {
|
||||
|
||||
@@ -1,7 +1,117 @@
|
||||
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.socket.CloseStatus
|
||||
import org.springframework.web.socket.TextMessage
|
||||
import org.springframework.web.socket.WebSocketSession
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler
|
||||
|
||||
@Component
|
||||
class UserCreatorChatWebSocketHandler : TextWebSocketHandler()
|
||||
class UserCreatorChatWebSocketHandler(
|
||||
private val service: UserCreatorChatService,
|
||||
private val presenceService: UserCreatorChatPresenceService,
|
||||
private val sessionRegistry: UserCreatorChatWebSocketSessionRegistry,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : TextWebSocketHandler() {
|
||||
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
|
||||
val request = objectMapper.readValue(message.payload, UserCreatorChatWebSocketMessage::class.java)
|
||||
when (request.type) {
|
||||
UserCreatorChatWebSocketMessageType.JOIN_ROOM -> handleJoinRoom(session, request)
|
||||
UserCreatorChatWebSocketMessageType.SEND_TEXT -> handleSendText(session, request)
|
||||
else -> sendError(session, request, "common.error.invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleJoinRoom(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||
try {
|
||||
val memberId = memberId(session)
|
||||
service.validateParticipant(roomId = request.roomId, memberId = memberId)
|
||||
clearJoinedRoom(session)
|
||||
sessionRegistry.register(roomId = request.roomId, memberId = memberId, session = session)
|
||||
presenceService.markJoined(roomId = request.roomId, memberId = memberId, sessionId = session.id)
|
||||
session.attributes[JOINED_ROOM_ID_ATTRIBUTE] = request.roomId
|
||||
sendEnvelope(
|
||||
session,
|
||||
UserCreatorChatWebSocketMessageType.JOINED,
|
||||
request.requestId,
|
||||
request.roomId,
|
||||
emptyMap<String, Any>()
|
||||
)
|
||||
} catch (e: SodaException) {
|
||||
sendError(session, request, e.messageKey ?: "common.error.invalid_request")
|
||||
session.close(CloseStatus.POLICY_VIOLATION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendText(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||
try {
|
||||
if (session.attributes[JOINED_ROOM_ID_ATTRIBUTE] != request.roomId) {
|
||||
throw SodaException(messageKey = "chat.room.join_required")
|
||||
}
|
||||
val textMessage = request.payload["textMessage"]?.asText().orEmpty()
|
||||
val message = service.sendTextMessageByWebSocket(
|
||||
memberId = memberId(session),
|
||||
roomId = request.roomId,
|
||||
textMessage = textMessage
|
||||
)
|
||||
sendEnvelope(session, UserCreatorChatWebSocketMessageType.SEND_ACK, request.requestId, request.roomId, message)
|
||||
} catch (e: SodaException) {
|
||||
sendError(session, request, e.messageKey ?: "common.error.invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
|
||||
clearJoinedRoom(session)
|
||||
}
|
||||
|
||||
private fun memberId(session: WebSocketSession): Long {
|
||||
return session.attributes[UserCreatorChatWebSocketAuthInterceptor.MEMBER_ID_ATTRIBUTE] as? Long
|
||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
}
|
||||
|
||||
private fun clearJoinedRoom(session: WebSocketSession) {
|
||||
val roomId = session.attributes.remove(JOINED_ROOM_ID_ATTRIBUTE) as? Long
|
||||
if (roomId != null) {
|
||||
presenceService.markLeft(roomId = roomId, memberId = memberId(session), sessionId = session.id)
|
||||
}
|
||||
sessionRegistry.remove(session.id)
|
||||
}
|
||||
|
||||
private fun sendError(session: WebSocketSession, request: UserCreatorChatWebSocketMessage, messageKey: String) {
|
||||
sendEnvelope(
|
||||
session,
|
||||
UserCreatorChatWebSocketMessageType.ERROR,
|
||||
request.requestId,
|
||||
request.roomId,
|
||||
mapOf("messageKey" to messageKey)
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendEnvelope(
|
||||
session: WebSocketSession,
|
||||
type: UserCreatorChatWebSocketMessageType,
|
||||
requestId: String?,
|
||||
roomId: Long,
|
||||
payload: Any
|
||||
) {
|
||||
session.sendMessage(
|
||||
TextMessage(
|
||||
objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"type" to type,
|
||||
"requestId" to requestId,
|
||||
"roomId" to roomId,
|
||||
"payload" to payload
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val JOINED_ROOM_ID_ATTRIBUTE = "userCreatorChatJoinedRoomId"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatPar
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatRealtimeService
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceService
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBroker
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
@@ -34,6 +36,8 @@ class UserCreatorChatServiceTest {
|
||||
private lateinit var memberRepository: MemberRepository
|
||||
private lateinit var blockMemberRepository: BlockMemberRepository
|
||||
private lateinit var realtimeService: UserCreatorChatRealtimeService
|
||||
private lateinit var presenceService: UserCreatorChatPresenceService
|
||||
private lateinit var roomMessageBroker: UserCreatorChatRoomMessageBroker
|
||||
private lateinit var eventPublisher: ApplicationEventPublisher
|
||||
private lateinit var service: UserCreatorChatService
|
||||
|
||||
@@ -45,6 +49,8 @@ class UserCreatorChatServiceTest {
|
||||
memberRepository = Mockito.mock(MemberRepository::class.java)
|
||||
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java)
|
||||
realtimeService = Mockito.mock(UserCreatorChatRealtimeService::class.java)
|
||||
presenceService = Mockito.mock(UserCreatorChatPresenceService::class.java)
|
||||
roomMessageBroker = Mockito.mock(UserCreatorChatRoomMessageBroker::class.java)
|
||||
eventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
|
||||
service = UserCreatorChatService(
|
||||
@@ -54,6 +60,8 @@ class UserCreatorChatServiceTest {
|
||||
memberRepository = memberRepository,
|
||||
blockMemberRepository = blockMemberRepository,
|
||||
realtimeService = realtimeService,
|
||||
presenceService = presenceService,
|
||||
roomMessageBroker = roomMessageBroker,
|
||||
applicationEventPublisher = eventPublisher,
|
||||
objectMapper = ObjectMapper(),
|
||||
s3Uploader = Mockito.mock(S3Uploader::class.java),
|
||||
@@ -215,6 +223,34 @@ class UserCreatorChatServiceTest {
|
||||
Mockito.verifyNoInteractions(eventPublisher)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("WebSocket 텍스트 전송은 상대방 presence가 있으면 메시지를 저장하고 broker로 MESSAGE를 발행하며 푸시를 보내지 않는다")
|
||||
fun shouldPublishWebSocketMessageWithoutPushWhenOpponentPresenceExists() {
|
||||
val user = member(1L, "user")
|
||||
val creator = member(2L, "creator")
|
||||
val room = room(10L)
|
||||
val senderParticipant = participant(100L, room, user)
|
||||
val recipientParticipant = participant(101L, room, creator)
|
||||
Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room)
|
||||
Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(senderParticipant)
|
||||
Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(recipientParticipant)
|
||||
Mockito.`when`(presenceService.hasPresence(10L, 2L)).thenReturn(true)
|
||||
Mockito.`when`(messageRepository.save(Mockito.any(UserCreatorChatMessage::class.java))).thenAnswer { invocation ->
|
||||
(invocation.arguments[0] as UserCreatorChatMessage).apply { id = 203L }
|
||||
}
|
||||
|
||||
val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello")
|
||||
|
||||
assertEquals(203L, response.messageId)
|
||||
assertEquals("hello", response.textMessage)
|
||||
assertTrue(response.mine)
|
||||
val payloadCaptor = ArgumentCaptor.forClass(String::class.java)
|
||||
Mockito.verify(roomMessageBroker).publish(Mockito.eq(10L), Mockito.eq(2L), captureString(payloadCaptor))
|
||||
assertTrue(payloadCaptor.value.contains("\"type\":\"MESSAGE\""))
|
||||
assertTrue(payloadCaptor.value.contains("\"messageId\":203"))
|
||||
Mockito.verifyNoInteractions(eventPublisher)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커서가 있으면 기본 20개 기준으로 이전 메시지를 조회한다")
|
||||
fun shouldGetPreviousMessagesWithDefaultLimitWhenCursorExists() {
|
||||
@@ -291,6 +327,10 @@ class UserCreatorChatServiceTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun captureString(captor: ArgumentCaptor<String>): String {
|
||||
return captor.capture() ?: ""
|
||||
}
|
||||
|
||||
private fun room(id: Long) = UserCreatorChatRoom().apply {
|
||||
this.id = id
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
@@ -27,6 +29,18 @@ class UserCreatorChatWebSocketConfigTest @Autowired constructor(
|
||||
@MockBean
|
||||
private lateinit var tokenProvider: TokenProvider
|
||||
|
||||
@MockBean
|
||||
private lateinit var service: UserCreatorChatService
|
||||
|
||||
@MockBean
|
||||
private lateinit var presenceService: UserCreatorChatPresenceService
|
||||
|
||||
@MockBean
|
||||
private lateinit var sessionRegistry: UserCreatorChatWebSocketSessionRegistry
|
||||
|
||||
@MockBean
|
||||
private lateinit var objectMapper: ObjectMapper
|
||||
|
||||
@Test
|
||||
@DisplayName("유저-크리에이터 채팅 WebSocket handler를 지정 경로와 인증 interceptor, origin 제한으로 등록한다")
|
||||
fun shouldRegisterUserCreatorChatWebSocketHandler() {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.web.socket.CloseStatus
|
||||
import org.springframework.web.socket.TextMessage
|
||||
import org.springframework.web.socket.WebSocketSession
|
||||
|
||||
class UserCreatorChatWebSocketHandlerTest {
|
||||
private val service = Mockito.mock(UserCreatorChatService::class.java)
|
||||
private val presenceService = Mockito.mock(UserCreatorChatPresenceService::class.java)
|
||||
private val sessionRegistry = UserCreatorChatWebSocketSessionRegistry()
|
||||
private val objectMapper = ObjectMapper().findAndRegisterModules()
|
||||
private val handler = UserCreatorChatWebSocketHandler(
|
||||
service = service,
|
||||
presenceService = presenceService,
|
||||
sessionRegistry = sessionRegistry,
|
||||
objectMapper = objectMapper
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("JOIN_ROOM은 참여자 검증 후 local session과 Redis presence를 등록하고 JOINED를 응답한다")
|
||||
fun shouldJoinRoomAndRegisterPresenceWhenParticipantIsValid() {
|
||||
val session = session("session-1", memberId = 1L)
|
||||
|
||||
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||
|
||||
Mockito.verify(service).validateParticipant(roomId = 10L, memberId = 1L)
|
||||
assertEquals(listOf(session), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
||||
Mockito.verify(presenceService).markJoined(roomId = 10L, memberId = 1L, sessionId = "session-1")
|
||||
val response = sentJson(session)
|
||||
assertEquals("JOINED", response["type"].asText())
|
||||
assertEquals("join-1", response["requestId"].asText())
|
||||
assertEquals(10L, response["roomId"].asLong())
|
||||
assertTrue(response["payload"].isObject)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JOIN_ROOM 요청자가 참여자가 아니면 ERROR 응답 후 WebSocket을 닫는다")
|
||||
fun shouldSendErrorAndCloseWhenJoinRoomParticipantIsInvalid() {
|
||||
val session = session("session-1", memberId = 1L)
|
||||
Mockito.doThrow(SodaException(messageKey = "chat.room.invalid_access"))
|
||||
.`when`(service)
|
||||
.validateParticipant(roomId = 10L, memberId = 1L)
|
||||
|
||||
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||
|
||||
val response = sentJson(session)
|
||||
assertEquals("ERROR", response["type"].asText())
|
||||
assertEquals("join-1", response["requestId"].asText())
|
||||
assertEquals(10L, response["roomId"].asLong())
|
||||
assertEquals("chat.room.invalid_access", response["payload"]["messageKey"].asText())
|
||||
Mockito.verify(session).close(CloseStatus.POLICY_VIOLATION)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SEND_TEXT는 메시지 저장 후 sender에게 SEND_ACK를 응답한다")
|
||||
fun shouldSendAckToSenderWhenTextMessageIsSaved() {
|
||||
val session = session("session-1", memberId = 1L)
|
||||
Mockito.`when`(service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello"))
|
||||
.thenReturn(messageItem())
|
||||
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||
Mockito.clearInvocations(session)
|
||||
|
||||
handler.handleMessage(
|
||||
session,
|
||||
textMessage("SEND_TEXT", "send-1", 10L, "{\"textMessage\":\"hello\"}")
|
||||
)
|
||||
|
||||
val response = sentJson(session)
|
||||
assertEquals("SEND_ACK", response["type"].asText())
|
||||
assertEquals("send-1", response["requestId"].asText())
|
||||
assertEquals(10L, response["roomId"].asLong())
|
||||
assertEquals(200L, response["payload"]["messageId"].asLong())
|
||||
assertEquals("hello", response["payload"]["textMessage"].asText())
|
||||
assertTrue(response["payload"]["mine"].asBoolean())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SEND_TEXT는 JOIN_ROOM 완료 전이면 저장하지 않고 ERROR를 응답한다")
|
||||
fun shouldRejectSendTextBeforeJoinRoom() {
|
||||
val session = session("session-1", memberId = 1L)
|
||||
|
||||
handler.handleMessage(
|
||||
session,
|
||||
textMessage("SEND_TEXT", "send-1", 10L, "{\"textMessage\":\"hello\"}")
|
||||
)
|
||||
|
||||
val response = sentJson(session)
|
||||
assertEquals("ERROR", response["type"].asText())
|
||||
assertEquals("send-1", response["requestId"].asText())
|
||||
assertEquals(10L, response["roomId"].asLong())
|
||||
assertEquals("chat.room.join_required", response["payload"]["messageKey"].asText())
|
||||
Mockito.verify(service, Mockito.never()).sendTextMessageByWebSocket(
|
||||
Mockito.anyLong(),
|
||||
Mockito.anyLong(),
|
||||
Mockito.anyString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("같은 session이 다른 방에 JOIN_ROOM하면 기존 방 presence를 제거하고 새 방만 등록한다")
|
||||
fun shouldRemovePreviousPresenceWhenSessionJoinsAnotherRoom() {
|
||||
val session = session("session-1", memberId = 1L)
|
||||
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||
|
||||
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-2", 20L, "{}"))
|
||||
|
||||
Mockito.verify(presenceService).markLeft(roomId = 10L, memberId = 1L, sessionId = "session-1")
|
||||
Mockito.verify(presenceService).markJoined(roomId = 20L, memberId = 1L, sessionId = "session-1")
|
||||
assertEquals(emptyList<WebSocketSession>(), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
||||
assertEquals(listOf(session), sessionRegistry.findSessions(roomId = 20L, memberId = 1L))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("WebSocket close 시 local session과 Redis presence를 제거한다")
|
||||
fun shouldRemoveLocalSessionAndPresenceWhenConnectionCloses() {
|
||||
val session = session("session-1", memberId = 1L)
|
||||
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
|
||||
|
||||
handler.afterConnectionClosed(session, CloseStatus.NORMAL)
|
||||
|
||||
Mockito.verify(presenceService).markLeft(roomId = 10L, memberId = 1L, sessionId = "session-1")
|
||||
assertEquals(emptyList<WebSocketSession>(), sessionRegistry.findSessions(roomId = 10L, memberId = 1L))
|
||||
}
|
||||
|
||||
private fun textMessage(type: String, requestId: String, roomId: Long, payload: String): TextMessage {
|
||||
return TextMessage(
|
||||
"""
|
||||
{
|
||||
"type": "$type",
|
||||
"requestId": "$requestId",
|
||||
"roomId": $roomId,
|
||||
"payload": $payload
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun session(id: String, memberId: Long): WebSocketSession {
|
||||
val session = Mockito.mock(WebSocketSession::class.java)
|
||||
Mockito.`when`(session.id).thenReturn(id)
|
||||
val attributes = mutableMapOf<String, Any>(UserCreatorChatWebSocketAuthInterceptor.MEMBER_ID_ATTRIBUTE to memberId)
|
||||
Mockito.`when`(session.attributes).thenReturn(attributes)
|
||||
Mockito.`when`(session.isOpen).thenReturn(true)
|
||||
return session
|
||||
}
|
||||
|
||||
private fun sentJson(session: WebSocketSession): JsonNode {
|
||||
val captor = ArgumentCaptor.forClass(TextMessage::class.java)
|
||||
Mockito.verify(session).sendMessage(captor.capture())
|
||||
return objectMapper.readTree(captor.value.payload)
|
||||
}
|
||||
|
||||
private fun messageItem() = UserCreatorChatMessageItemDto(
|
||||
messageId = 200L,
|
||||
messageType = "TEXT",
|
||||
mine = true,
|
||||
createdAt = 1781690401000L,
|
||||
textMessage = "hello",
|
||||
voiceMessageUrl = null,
|
||||
senderId = 1L,
|
||||
senderNickname = "user",
|
||||
senderProfileImageUrl = "https://cdn.test/profile/user.png"
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user