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

This commit is contained in:
2026-06-18 19:07:54 +09:00
parent afa57b70de
commit 216850c07a
3 changed files with 248 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
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.core.SetOperations
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.core.ValueOperations
import java.time.Duration
import java.time.Instant
class UserCreatorChatPresenceServiceTest {
private val stringRedisTemplate = Mockito.mock(StringRedisTemplate::class.java)
private val valueOperations = Mockito.mock(ValueOperations::class.java) as ValueOperations<String, String>
private val setOperations = Mockito.mock(SetOperations::class.java) as SetOperations<String, String>
private val objectMapper = ObjectMapper()
.findAndRegisterModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
private val service = UserCreatorChatPresenceService(stringRedisTemplate, objectMapper, "test-server")
@Test
@DisplayName("join 시 session presence와 member session index에 TTL을 설정한다")
fun shouldMarkJoinedWithTtl() {
givenRedisOperations()
service.markJoined(roomId = 10L, memberId = 20L, sessionId = "session-1")
val presenceJsonCaptor = ArgumentCaptor.forClass(String::class.java)
Mockito.verify(valueOperations).set(
Mockito.eq("v2:user-creator-chat:ws:presence:10:20:session-1"),
presenceJsonCaptor.capture(),
Mockito.eq(Duration.ofSeconds(90))
)
assertPresenceJson(presenceJsonCaptor.value)
Mockito.verify(setOperations).add("v2:user-creator-chat:ws:room:10:member:20:sessions", "session-1")
Mockito.verify(stringRedisTemplate).expire(
"v2:user-creator-chat:ws:room:10:member:20:sessions",
Duration.ofSeconds(90)
)
Mockito.verify(setOperations).add("v2:user-creator-chat:ws:room:10", "20")
Mockito.verify(stringRedisTemplate).expire("v2:user-creator-chat:ws:room:10", Duration.ofSeconds(90))
}
@Test
@DisplayName("refresh 시 기존 session presence와 index TTL을 갱신한다")
fun shouldRefreshPresenceTtl() {
givenRedisOperations()
service.refresh(roomId = 10L, memberId = 20L, sessionId = "session-1")
val presenceJsonCaptor = ArgumentCaptor.forClass(String::class.java)
Mockito.verify(valueOperations).set(
Mockito.eq("v2:user-creator-chat:ws:presence:10:20:session-1"),
presenceJsonCaptor.capture(),
Mockito.eq(Duration.ofSeconds(90))
)
assertPresenceJson(presenceJsonCaptor.value)
Mockito.verify(stringRedisTemplate).expire(
"v2:user-creator-chat:ws:room:10:member:20:sessions",
Duration.ofSeconds(90)
)
Mockito.verify(stringRedisTemplate).expire("v2:user-creator-chat:ws:room:10", Duration.ofSeconds(90))
}
@Test
@DisplayName("leave 시 session presence를 삭제하고 마지막 session이면 member presence를 제거한다")
fun shouldMarkLeftAndRemoveMemberPresenceWhenLastSessionLeaves() {
givenRedisOperations()
Mockito.`when`(setOperations.size("v2:user-creator-chat:ws:room:10:member:20:sessions")).thenReturn(0L)
service.markLeft(roomId = 10L, memberId = 20L, sessionId = "session-1")
Mockito.verify(stringRedisTemplate).delete("v2:user-creator-chat:ws:presence:10:20:session-1")
Mockito.verify(setOperations).remove("v2:user-creator-chat:ws:room:10:member:20:sessions", "session-1")
Mockito.verify(stringRedisTemplate).delete("v2:user-creator-chat:ws:room:10:member:20:sessions")
Mockito.verify(setOperations).remove("v2:user-creator-chat:ws:room:10", "20")
}
@Test
@DisplayName("leave 후 남은 session id가 stale이면 member presence를 제거한다")
fun shouldRemoveMemberPresenceWhenRemainingSessionIdsAreStale() {
givenRedisOperations()
Mockito.`when`(setOperations.size("v2:user-creator-chat:ws:room:10:member:20:sessions")).thenReturn(1L)
Mockito.`when`(setOperations.members("v2:user-creator-chat:ws:room:10:member:20:sessions"))
.thenReturn(setOf("session-2"))
Mockito.`when`(stringRedisTemplate.hasKey("v2:user-creator-chat:ws:presence:10:20:session-2"))
.thenReturn(false)
service.markLeft(roomId = 10L, memberId = 20L, sessionId = "session-1")
Mockito.verify(setOperations).remove("v2:user-creator-chat:ws:room:10:member:20:sessions", "session-2")
Mockito.verify(stringRedisTemplate).delete("v2:user-creator-chat:ws:room:10:member:20:sessions")
Mockito.verify(setOperations).remove("v2:user-creator-chat:ws:room:10", "20")
}
@Test
@DisplayName("member session index에 live session key가 있으면 room presence가 있다고 판단한다")
fun shouldReturnPresenceFromMemberSessionIndex() {
givenRedisOperations()
Mockito.`when`(setOperations.members("v2:user-creator-chat:ws:room:10:member:20:sessions"))
.thenReturn(setOf("session-1"))
Mockito.`when`(stringRedisTemplate.hasKey("v2:user-creator-chat:ws:presence:10:20:session-1"))
.thenReturn(true)
val hasPresence = service.hasPresence(roomId = 10L, memberId = 20L)
org.junit.jupiter.api.Assertions.assertEquals(true, hasPresence, "Expected member presence to be true")
}
private fun givenRedisOperations() {
Mockito.`when`(stringRedisTemplate.opsForValue()).thenReturn(valueOperations)
Mockito.`when`(stringRedisTemplate.opsForSet()).thenReturn(setOperations)
}
private fun assertPresenceJson(json: String) {
val presence = objectMapper.readTree(json)
assertEquals("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()))
}
}