test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
5 changed files with 265 additions and 0 deletions
Showing only changes of commit a170c82a92 - Show all commits

View File

@@ -0,0 +1,63 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import kr.co.vividnext.sodalive.jwt.JwtFilter
import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.MemberAdapter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.stereotype.Component
import org.springframework.util.StringUtils
import org.springframework.web.socket.WebSocketHandler
import org.springframework.web.socket.server.HandshakeInterceptor
@Component
class UserCreatorChatWebSocketAuthInterceptor(
private val tokenProvider: TokenProvider
) : HandshakeInterceptor {
override fun beforeHandshake(
request: ServerHttpRequest,
response: ServerHttpResponse,
wsHandler: WebSocketHandler,
attributes: MutableMap<String, Any>
): Boolean {
val token = resolveToken(request) ?: return false
if (!tokenProvider.validateToken(token)) {
return false
}
val authentication = try {
tokenProvider.getAuthentication(token)
} catch (e: RuntimeException) {
return false
}
val principal = authentication.principal as? MemberAdapter ?: return false
val memberId = principal.member.id ?: return false
attributes[MEMBER_ID_ATTRIBUTE] = memberId
attributes[AUTHENTICATION_ATTRIBUTE] = authentication
return true
}
override fun afterHandshake(
request: ServerHttpRequest,
response: ServerHttpResponse,
wsHandler: WebSocketHandler,
exception: Exception?
) {
}
private fun resolveToken(request: ServerHttpRequest): String? {
val bearerToken = request.headers.getFirst(JwtFilter.AUTHORIZATION_HEADER)
if (StringUtils.hasText(bearerToken) && bearerToken!!.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length)
}
return null
}
companion object {
const val MEMBER_ID_ATTRIBUTE = "memberId"
const val AUTHENTICATION_ATTRIBUTE = "authentication"
private const val BEARER_PREFIX = "Bearer "
}
}

View File

@@ -0,0 +1,29 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import org.springframework.context.annotation.Configuration
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@Configuration
@EnableWebSocket
class UserCreatorChatWebSocketConfig(
private val handler: UserCreatorChatWebSocketHandler,
private val authInterceptor: UserCreatorChatWebSocketAuthInterceptor
) : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(handler, ENDPOINT)
.addInterceptors(authInterceptor)
.setAllowedOrigins(
"http://localhost:8888",
"https://creator.sodalive.net",
"https://test-creator.sodalive.net",
"https://test-admin.sodalive.net",
"https://admin.sodalive.net"
)
}
companion object {
const val ENDPOINT = "/ws/v2/user-creator-chat"
}
}

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import org.springframework.stereotype.Component
import org.springframework.web.socket.handler.TextWebSocketHandler
@Component
class UserCreatorChatWebSocketHandler : TextWebSocketHandler()

View File

@@ -0,0 +1,111 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberAdapter
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
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.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.http.HttpHeaders
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.http.server.ServletServerHttpRequest
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.web.socket.WebSocketHandler
class UserCreatorChatWebSocketAuthInterceptorTest {
private val tokenProvider = Mockito.mock(TokenProvider::class.java)
private val interceptor = UserCreatorChatWebSocketAuthInterceptor(tokenProvider)
private val response = Mockito.mock(ServerHttpResponse::class.java)
private val wsHandler = Mockito.mock(WebSocketHandler::class.java)
@Test
@DisplayName("Authorization Bearer token이 유효하면 handshake attributes에 인증 정보를 저장한다")
fun shouldStoreAuthenticationAttributesWhenBearerTokenIsValid() {
val member = Member(email = "viewer@test.com", password = "password", nickname = "viewer").apply { id = 10L }
val authentication = UsernamePasswordAuthenticationToken(MemberAdapter(member), "valid-token")
Mockito.`when`(tokenProvider.validateToken("valid-token")).thenReturn(true)
Mockito.`when`(tokenProvider.getAuthentication("valid-token")).thenReturn(authentication)
val attributes = mutableMapOf<String, Any>()
val result = interceptor.beforeHandshake(
requestWithAuthorization("Bearer valid-token"),
response,
wsHandler,
attributes
)
assertTrue(result, "Expected valid Bearer token handshake to proceed")
assertEquals(10L, attributes[UserCreatorChatWebSocketAuthInterceptor.MEMBER_ID_ATTRIBUTE])
assertSame(authentication, attributes[UserCreatorChatWebSocketAuthInterceptor.AUTHENTICATION_ATTRIBUTE])
}
@Test
@DisplayName("Authorization header가 없으면 handshake를 거부한다")
fun shouldRejectHandshakeWithoutAuthorizationHeader() {
val attributes = mutableMapOf<String, Any>()
val result = interceptor.beforeHandshake(
requestWithAuthorization(null),
response,
wsHandler,
attributes
)
assertFalse(result, "Expected missing Authorization header handshake to be rejected")
assertTrue(attributes.isEmpty(), "Expected rejected handshake to leave attributes empty")
}
@Test
@DisplayName("유효하지 않은 Bearer token이면 handshake를 거부한다")
fun shouldRejectHandshakeWhenBearerTokenIsInvalid() {
Mockito.`when`(tokenProvider.validateToken("invalid-token")).thenReturn(false)
val attributes = mutableMapOf<String, Any>()
val result = interceptor.beforeHandshake(
requestWithAuthorization("Bearer invalid-token"),
response,
wsHandler,
attributes
)
assertFalse(result, "Expected invalid Bearer token handshake to be rejected")
assertTrue(attributes.isEmpty(), "Expected rejected handshake to leave attributes empty")
}
@Test
@DisplayName("토큰 검증 후 인증 정보 조회가 실패하면 handshake를 거부한다")
fun shouldRejectHandshakeWhenAuthenticationLookupFails() {
Mockito.`when`(tokenProvider.validateToken("logged-out-token")).thenReturn(true)
Mockito.`when`(tokenProvider.getAuthentication("logged-out-token"))
.thenThrow(IllegalStateException("token not found"))
val attributes = mutableMapOf<String, Any>()
var result = true
assertDoesNotThrow {
result = interceptor.beforeHandshake(
requestWithAuthorization("Bearer logged-out-token"),
response,
wsHandler,
attributes
)
}
assertFalse(result, "Expected authentication lookup failure handshake to be rejected")
assertTrue(attributes.isEmpty(), "Expected rejected handshake to leave attributes empty")
}
private fun requestWithAuthorization(authorization: String?): ServerHttpRequest {
val request = MockHttpServletRequest()
if (authorization != null) {
request.addHeader(HttpHeaders.AUTHORIZATION, authorization)
}
return ServletServerHttpRequest(request)
}
}

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import kr.co.vividnext.sodalive.jwt.TokenProvider
import org.junit.jupiter.api.Assertions.assertEquals
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.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.ApplicationContext
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
import org.springframework.web.socket.server.support.OriginHandshakeInterceptor
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler
@SpringBootTest(
classes = [
UserCreatorChatWebSocketConfig::class,
UserCreatorChatWebSocketHandler::class,
UserCreatorChatWebSocketAuthInterceptor::class
]
)
class UserCreatorChatWebSocketConfigTest @Autowired constructor(
private val applicationContext: ApplicationContext
) {
@MockBean
private lateinit var tokenProvider: TokenProvider
@Test
@DisplayName("유저-크리에이터 채팅 WebSocket handler를 지정 경로와 인증 interceptor, origin 제한으로 등록한다")
fun shouldRegisterUserCreatorChatWebSocketHandler() {
val handlerMappings = applicationContext.getBeansOfType(SimpleUrlHandlerMapping::class.java).values
val urlMap = handlerMappings.flatMap { mapping -> mapping.urlMap.entries }
val handler = urlMap.firstNotNullOfOrNull { (path, handler) ->
if (path == "/ws/v2/user-creator-chat") handler as? WebSocketHttpRequestHandler else null
}
assertNotNull(handler, "Expected /ws/v2/user-creator-chat to be registered")
val interceptors = handler!!.handshakeInterceptors
assertTrue(interceptors.any { it is UserCreatorChatWebSocketAuthInterceptor })
val originInterceptor = interceptors.filterIsInstance<OriginHandshakeInterceptor>().single()
assertEquals(
listOf(
"http://localhost:8888",
"https://creator.sodalive.net",
"https://test-creator.sodalive.net",
"https://test-admin.sodalive.net",
"https://admin.sodalive.net"
),
originInterceptor.allowedOrigins.toList()
)
}
}