test #426
@@ -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