feat(user-creator-chat): WebSocket Redis room broker를 추가한다

This commit is contained in:
2026-06-18 19:08:16 +09:00
parent 216850c07a
commit f44ea58ca2
3 changed files with 186 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.springframework.data.redis.connection.Message
import org.springframework.data.redis.connection.MessageListener
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.listener.PatternTopic
import org.springframework.data.redis.listener.RedisMessageListenerContainer
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import java.io.IOException
import java.nio.charset.StandardCharsets
class UserCreatorChatRoomMessageBrokerTest {
private val stringRedisTemplate = Mockito.mock(StringRedisTemplate::class.java)
private val registry = UserCreatorChatWebSocketSessionRegistry()
private val listenerContainer = Mockito.mock(RedisMessageListenerContainer::class.java)
private val objectMapper = ObjectMapper().findAndRegisterModules()
private val broker = UserCreatorChatRoomMessageBroker(
stringRedisTemplate = stringRedisTemplate,
sessionRegistry = registry,
objectMapper = objectMapper,
listenerContainer = listenerContainer
)
@Test
@DisplayName("room channel로 target member와 payload를 publish한다")
fun shouldPublishMessageToRoomChannel() {
broker.publish(roomId = 10L, memberId = 20L, payload = "{\"type\":\"MESSAGE\"}")
val messageCaptor = ArgumentCaptor.forClass(String::class.java)
Mockito.verify(stringRedisTemplate).convertAndSend(
Mockito.eq("v2:user-creator-chat:ws:room:10"),
messageCaptor.capture()
)
val published = objectMapper.readValue(messageCaptor.value, UserCreatorChatRoomPublishedMessage::class.java)
assertEquals(10L, published.roomId)
assertEquals(20L, published.memberId)
assertEquals("{\"type\":\"MESSAGE\"}", published.payload)
}
@Test
@DisplayName("생성 시 ws room pattern topic을 구독한다")
fun shouldSubscribeRoomPatternOnCreation() {
Mockito.verify(listenerContainer).addMessageListener(
Mockito.any(MessageListener::class.java),
Mockito.eq(PatternTopic("v2:user-creator-chat:ws:room:*"))
)
}
@Test
@DisplayName("subscribe callback은 대상 member의 local session에만 메시지를 전송한다")
fun shouldDeliverSubscribedMessageOnlyToTargetMemberSessions() {
val targetSession = session("target-session")
val otherMemberSession = session("other-session")
registry.register(roomId = 10L, memberId = 20L, session = targetSession)
registry.register(roomId = 10L, memberId = 21L, session = otherMemberSession)
val published = UserCreatorChatRoomPublishedMessage(
roomId = 10L,
memberId = 20L,
payload = "{\"type\":\"MESSAGE\"}"
)
broker.onMessage(redisMessage(objectMapper.writeValueAsString(published)), null)
val textCaptor = ArgumentCaptor.forClass(TextMessage::class.java)
Mockito.verify(targetSession).sendMessage(textCaptor.capture())
assertEquals("{\"type\":\"MESSAGE\"}", textCaptor.value.payload)
Mockito.verify(otherMemberSession, Mockito.never()).sendMessage(Mockito.any(TextMessage::class.java))
}
@Test
@DisplayName("일부 local session 전송이 실패해도 같은 member의 다른 session 전송을 계속한다")
fun shouldContinueDeliveryWhenOneTargetSessionFails() {
val brokenSession = session("broken-session")
val healthySession = session("healthy-session")
Mockito.doThrow(IOException("broken socket"))
.`when`(brokenSession)
.sendMessage(Mockito.any(TextMessage::class.java))
registry.register(roomId = 10L, memberId = 20L, session = brokenSession)
registry.register(roomId = 10L, memberId = 20L, session = healthySession)
val published = UserCreatorChatRoomPublishedMessage(
roomId = 10L,
memberId = 20L,
payload = "{\"type\":\"MESSAGE\"}"
)
broker.onMessage(redisMessage(objectMapper.writeValueAsString(published)), null)
val textCaptor = ArgumentCaptor.forClass(TextMessage::class.java)
Mockito.verify(healthySession).sendMessage(textCaptor.capture())
assertEquals("{\"type\":\"MESSAGE\"}", textCaptor.value.payload)
}
private fun redisMessage(body: String): Message {
val message = Mockito.mock(Message::class.java)
Mockito.`when`(message.body).thenReturn(body.toByteArray(StandardCharsets.UTF_8))
return message
}
private fun session(id: String): WebSocketSession {
val session = Mockito.mock(WebSocketSession::class.java)
Mockito.`when`(session.id).thenReturn(id)
Mockito.`when`(session.isOpen).thenReturn(true)
return session
}
}