test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
5 changed files with 396 additions and 9 deletions
Showing only changes of commit 7080a03166 - Show all commits

View File

@@ -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.UserCreatorChatMessageRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository 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.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.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
@@ -43,6 +46,8 @@ class UserCreatorChatService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val blockMemberRepository: BlockMemberRepository, private val blockMemberRepository: BlockMemberRepository,
private val realtimeService: UserCreatorChatRealtimeService, private val realtimeService: UserCreatorChatRealtimeService,
private val presenceService: UserCreatorChatPresenceService,
private val roomMessageBroker: UserCreatorChatRoomMessageBroker,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
@@ -114,17 +119,30 @@ class UserCreatorChatService(
): SendUserCreatorChatMessageResponse { ): SendUserCreatorChatMessageResponse {
if (request.textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request") if (request.textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
val context = resolveSendContext(member, roomId) val context = resolveSendContext(member, roomId)
val message = messageRepository.save( val message = saveTextMessage(context, request.textMessage)
UserCreatorChatMessage(
chatRoom = context.room,
participant = context.senderParticipant,
messageType = UserCreatorChatMessageType.TEXT,
textMessage = request.textMessage
)
)
return deliverMessage(message, member, context.opponentParticipant) 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 @Transactional
fun sendVoiceMessage( fun sendVoiceMessage(
member: Member, member: Member,
@@ -162,6 +180,21 @@ class UserCreatorChatService(
realtimeService.disconnect(roomId, member.id!!) 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 { private fun resolveSendContext(member: Member, roomId: Long): SendContext {
val room = findRoom(roomId) val room = findRoom(roomId)
val senderParticipant = requireParticipant(roomId, member.id!!) 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) { private fun validateRecipient(sender: Member, recipient: Member) {
if (!recipient.isActive) throw SodaException(messageKey = "message.error.recipient_inactive") if (!recipient.isActive) throw SodaException(messageKey = "message.error.recipient_inactive")
if (recipient.memberKind == MemberKind.AI_CHARACTER) { if (recipient.memberKind == MemberKind.AI_CHARACTER) {

View File

@@ -1,7 +1,117 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket 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.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 import org.springframework.web.socket.handler.TextWebSocketHandler
@Component @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"
}
}

View File

@@ -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.repository.UserCreatorChatRoomRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatRealtimeService 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.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.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
@@ -34,6 +36,8 @@ class UserCreatorChatServiceTest {
private lateinit var memberRepository: MemberRepository private lateinit var memberRepository: MemberRepository
private lateinit var blockMemberRepository: BlockMemberRepository private lateinit var blockMemberRepository: BlockMemberRepository
private lateinit var realtimeService: UserCreatorChatRealtimeService private lateinit var realtimeService: UserCreatorChatRealtimeService
private lateinit var presenceService: UserCreatorChatPresenceService
private lateinit var roomMessageBroker: UserCreatorChatRoomMessageBroker
private lateinit var eventPublisher: ApplicationEventPublisher private lateinit var eventPublisher: ApplicationEventPublisher
private lateinit var service: UserCreatorChatService private lateinit var service: UserCreatorChatService
@@ -45,6 +49,8 @@ class UserCreatorChatServiceTest {
memberRepository = Mockito.mock(MemberRepository::class.java) memberRepository = Mockito.mock(MemberRepository::class.java)
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java) blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java)
realtimeService = Mockito.mock(UserCreatorChatRealtimeService::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) eventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
service = UserCreatorChatService( service = UserCreatorChatService(
@@ -54,6 +60,8 @@ class UserCreatorChatServiceTest {
memberRepository = memberRepository, memberRepository = memberRepository,
blockMemberRepository = blockMemberRepository, blockMemberRepository = blockMemberRepository,
realtimeService = realtimeService, realtimeService = realtimeService,
presenceService = presenceService,
roomMessageBroker = roomMessageBroker,
applicationEventPublisher = eventPublisher, applicationEventPublisher = eventPublisher,
objectMapper = ObjectMapper(), objectMapper = ObjectMapper(),
s3Uploader = Mockito.mock(S3Uploader::class.java), s3Uploader = Mockito.mock(S3Uploader::class.java),
@@ -215,6 +223,34 @@ class UserCreatorChatServiceTest {
Mockito.verifyNoInteractions(eventPublisher) 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 @Test
@DisplayName("커서가 있으면 기본 20개 기준으로 이전 메시지를 조회한다") @DisplayName("커서가 있으면 기본 20개 기준으로 이전 메시지를 조회한다")
fun shouldGetPreviousMessagesWithDefaultLimitWhenCursorExists() { 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 { private fun room(id: Long) = UserCreatorChatRoom().apply {
this.id = id this.id = id
} }

View File

@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket 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.jwt.TokenProvider
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
@@ -27,6 +29,18 @@ class UserCreatorChatWebSocketConfigTest @Autowired constructor(
@MockBean @MockBean
private lateinit var tokenProvider: TokenProvider 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 @Test
@DisplayName("유저-크리에이터 채팅 WebSocket handler를 지정 경로와 인증 interceptor, origin 제한으로 등록한다") @DisplayName("유저-크리에이터 채팅 WebSocket handler를 지정 경로와 인증 interceptor, origin 제한으로 등록한다")
fun shouldRegisterUserCreatorChatWebSocketHandler() { fun shouldRegisterUserCreatorChatWebSocketHandler() {

View File

@@ -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"
)
}