test(user-creator-chat): WebSocket Redis 통합 검증을 추가한다
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
|
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.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.web.socket.TextMessage
|
||||||
|
import org.springframework.web.socket.WebSocketSession
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||||
|
@TestPropertySource(properties = ["user-creator-chat.websocket.server-id=redis-test-server"])
|
||||||
|
class UserCreatorChatRedisIntegrationTest {
|
||||||
|
@Autowired
|
||||||
|
private lateinit var stringRedisTemplate: StringRedisTemplate
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var presenceService: UserCreatorChatPresenceService
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var sessionRegistry: UserCreatorChatWebSocketSessionRegistry
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var roomMessageBroker: UserCreatorChatRoomMessageBroker
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
sessionRegistry.remove("redis-integration-session")
|
||||||
|
sessionRegistry.remove("redis-integration-other-session")
|
||||||
|
stringRedisTemplate.connectionFactory?.connection?.use { connection ->
|
||||||
|
connection.flushDb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("embedded Redis에 join presence key와 index를 저장하고 TTL을 설정한다")
|
||||||
|
fun shouldStorePresenceKeysWithTtlOnJoin() {
|
||||||
|
presenceService.markJoined(roomId = 10L, memberId = 20L, sessionId = "session-1")
|
||||||
|
|
||||||
|
val presenceKey = UserCreatorChatPresenceService.presenceKey(10L, 20L, "session-1")
|
||||||
|
val memberSessionsKey = UserCreatorChatPresenceService.memberSessionsKey(10L, 20L)
|
||||||
|
val roomKey = UserCreatorChatPresenceService.roomKey(10L)
|
||||||
|
|
||||||
|
assertPresenceJson(stringRedisTemplate.opsForValue().get(presenceKey))
|
||||||
|
assertTrue(stringRedisTemplate.opsForSet().isMember(memberSessionsKey, "session-1") == true)
|
||||||
|
assertTrue(stringRedisTemplate.opsForSet().isMember(roomKey, "20") == true)
|
||||||
|
assertTrue(stringRedisTemplate.getExpire(presenceKey) > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("embedded Redis에서 마지막 session leave 시 presence key와 index를 정리한다")
|
||||||
|
fun shouldRemovePresenceKeysWhenLastSessionLeaves() {
|
||||||
|
presenceService.markJoined(roomId = 10L, memberId = 20L, sessionId = "session-1")
|
||||||
|
|
||||||
|
presenceService.markLeft(roomId = 10L, memberId = 20L, sessionId = "session-1")
|
||||||
|
|
||||||
|
val presenceKey = UserCreatorChatPresenceService.presenceKey(10L, 20L, "session-1")
|
||||||
|
val memberSessionsKey = UserCreatorChatPresenceService.memberSessionsKey(10L, 20L)
|
||||||
|
val roomKey = UserCreatorChatPresenceService.roomKey(10L)
|
||||||
|
assertFalse(stringRedisTemplate.hasKey(presenceKey) == true)
|
||||||
|
assertFalse(stringRedisTemplate.hasKey(memberSessionsKey) == true)
|
||||||
|
assertFalse(stringRedisTemplate.opsForSet().isMember(roomKey, "20") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("stale session id만 남으면 presence 없음으로 판단하고 stale id를 제거한다")
|
||||||
|
fun shouldPruneStaleSessionIdWhenCheckingPresence() {
|
||||||
|
val memberSessionsKey = UserCreatorChatPresenceService.memberSessionsKey(10L, 20L)
|
||||||
|
stringRedisTemplate.opsForSet().add(memberSessionsKey, "stale-session")
|
||||||
|
|
||||||
|
val hasPresence = presenceService.hasPresence(roomId = 10L, memberId = 20L)
|
||||||
|
|
||||||
|
assertFalse(hasPresence)
|
||||||
|
assertFalse(stringRedisTemplate.opsForSet().isMember(memberSessionsKey, "stale-session") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("publish는 embedded Redis pub/sub listener를 거쳐 대상 local session에 payload를 전달한다")
|
||||||
|
fun shouldPublishThroughRedisAndDeliverToLocalTargetSession() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val targetSession = session("redis-integration-session", latch)
|
||||||
|
val otherSession = session("redis-integration-other-session", CountDownLatch(1))
|
||||||
|
sessionRegistry.register(roomId = 10L, memberId = 20L, session = targetSession)
|
||||||
|
sessionRegistry.register(roomId = 10L, memberId = 21L, session = otherSession)
|
||||||
|
|
||||||
|
roomMessageBroker.publish(roomId = 10L, memberId = 20L, payload = "{\"type\":\"MESSAGE\"}")
|
||||||
|
|
||||||
|
assertTrue(latch.await(3, TimeUnit.SECONDS), "Expected Redis pub/sub payload to reach target session")
|
||||||
|
val textCaptor = ArgumentCaptor.forClass(TextMessage::class.java)
|
||||||
|
Mockito.verify(targetSession).sendMessage(textCaptor.capture())
|
||||||
|
assertEquals("{\"type\":\"MESSAGE\"}", textCaptor.value.payload)
|
||||||
|
Mockito.verify(otherSession, Mockito.never()).sendMessage(Mockito.any(TextMessage::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun session(id: String, latch: CountDownLatch): WebSocketSession {
|
||||||
|
val session = Mockito.mock(WebSocketSession::class.java)
|
||||||
|
Mockito.`when`(session.id).thenReturn(id)
|
||||||
|
Mockito.`when`(session.isOpen).thenReturn(true)
|
||||||
|
Mockito.doAnswer {
|
||||||
|
latch.countDown()
|
||||||
|
null
|
||||||
|
}.`when`(session).sendMessage(Mockito.any(TextMessage::class.java))
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertPresenceJson(json: String?) {
|
||||||
|
assertNotNull(json)
|
||||||
|
val presence = objectMapper.readTree(json)
|
||||||
|
assertEquals("redis-test-server", presence["serverId"].asText())
|
||||||
|
assertEquals(20L, presence["memberId"].asLong())
|
||||||
|
assertEquals(10L, presence["roomId"].asLong())
|
||||||
|
assertEquals("session-1", presence["sessionId"].asText())
|
||||||
|
assertNotNull(Instant.parse(presence["lastSeenAt"].asText()))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user