feat(user-creator-chat): WebSocket 세션 레지스트리를 추가한다
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user