feat(user-creator-chat): WebSocket 세션 레지스트리를 추가한다

This commit is contained in:
2026-06-18 17:06:32 +09:00
parent fefd62c63a
commit af1e9b565a
2 changed files with 184 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.web.socket.WebSocketSession
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
class UserCreatorChatWebSocketSessionRegistryTest {
private val registry = UserCreatorChatWebSocketSessionRegistry()
@Test
@DisplayName("roomId/memberId/sessionId 기준으로 local WebSocket session을 등록하고 조회한다")
fun shouldRegisterAndFindSessionsByRoomAndMember() {
val session = session("session-1")
registry.register(roomId = 10L, memberId = 20L, session = session)
val sessions = registry.findSessions(roomId = 10L, memberId = 20L)
assertEquals(1, sessions.size, "Expected one registered local WebSocket session")
assertSame(session, sessions.single())
}
@Test
@DisplayName("sessionId로 등록된 local WebSocket session을 제거한다")
fun shouldRemoveSessionBySessionId() {
val session = session("session-1")
registry.register(roomId = 10L, memberId = 20L, session = session)
registry.remove("session-1")
assertFalse(
registry.findSessions(roomId = 10L, memberId = 20L).isNotEmpty(),
"Expected removed WebSocket session not to be returned"
)
}
@Test
@DisplayName("같은 session이 다른 room으로 전환되면 기존 room 등록을 제거한다")
fun shouldRemovePreviousRoomWhenSameSessionSwitchesRoom() {
val session = session("session-1")
registry.register(roomId = 10L, memberId = 20L, session = session)
registry.register(roomId = 11L, memberId = 20L, session = session)
assertFalse(
registry.findSessions(roomId = 10L, memberId = 20L).isNotEmpty(),
"Expected previous room mapping to be removed when same session switches rooms"
)
assertEquals(listOf(session), registry.findSessions(roomId = 11L, memberId = 20L))
}
@Test
@DisplayName("room/member에 등록된 여러 local session을 모두 조회한다")
fun shouldFindMultipleSessionsForSameRoomMember() {
val first = session("session-1")
val second = session("session-2")
registry.register(roomId = 10L, memberId = 20L, session = first)
registry.register(roomId = 10L, memberId = 20L, session = second)
val sessions = registry.findSessions(roomId = 10L, memberId = 20L)
assertEquals(setOf(first, second), sessions.toSet())
}
@Test
@DisplayName("같은 session의 동시 room 전환에서도 stale room 등록을 남기지 않는다")
fun shouldNotLeaveStaleRoomMappingWhenSameSessionSwitchesRoomConcurrently() {
val session = sessionWithSynchronizedFirstTwoIdReads("session-1")
val executor = Executors.newFixedThreadPool(2)
try {
val first = executor.submit { registry.register(roomId = 10L, memberId = 20L, session = session) }
val second = executor.submit { registry.register(roomId = 11L, memberId = 20L, session = session) }
first.get(3, TimeUnit.SECONDS)
second.get(3, TimeUnit.SECONDS)
} finally {
executor.shutdownNow()
}
val registeredRooms = listOf(10L, 11L).filter { roomId ->
registry.findSessions(roomId = roomId, memberId = 20L).isNotEmpty()
}
assertEquals(
1,
registeredRooms.size,
"Expected concurrent same-session room switch to leave exactly one active room mapping"
)
}
@Test
@DisplayName("sessionId별 lock map을 유지하지 않는다")
fun shouldNotKeepPerSessionLockMap() {
val hasSessionLockMap = UserCreatorChatWebSocketSessionRegistry::class.java.declaredFields
.any { field -> field.name == "sessionLocks" }
assertFalse(
hasSessionLockMap,
"Expected registry not to keep a per-session lock map that can grow with WebSocket traffic"
)
}
private fun sessionWithSynchronizedFirstTwoIdReads(id: String): WebSocketSession {
val session = Mockito.mock(WebSocketSession::class.java)
val readCount = AtomicInteger()
val firstTwoReads = CountDownLatch(2)
Mockito.`when`(session.id).thenAnswer {
if (readCount.incrementAndGet() <= 2) {
firstTwoReads.countDown()
firstTwoReads.await(1, TimeUnit.SECONDS)
}
id
}
return session
}
private fun session(id: String): WebSocketSession {
val session = Mockito.mock(WebSocketSession::class.java)
Mockito.`when`(session.id).thenReturn(id)
return session
}
}