feat(ai-character): 관리자 API Phase 1 기반을 추가한다

This commit is contained in:
2026-07-26 05:01:34 +09:00
parent d3564f8c0d
commit 0d2742756f
19 changed files with 4108 additions and 16 deletions
@@ -0,0 +1,138 @@
package kr.co.vividnext.sodalive.jwt
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.security.Keys
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.token.MemberToken
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.mockito.Mockito
import java.util.Base64
import java.util.Date
import java.util.Optional
class TokenProviderTest {
private lateinit var tokenProvider: TokenProvider
private lateinit var memberRepository: MemberRepository
private lateinit var tokenRepository: MemberTokenRepository
@BeforeEach
fun setUp() {
memberRepository = Mockito.mock(MemberRepository::class.java)
tokenRepository = Mockito.mock(MemberTokenRepository::class.java)
tokenProvider = TokenProvider(
secret = SECRET,
tokenValidityInSeconds = 3600,
repository = memberRepository,
tokenRepository = tokenRepository
)
tokenProvider.afterPropertiesSet()
}
@Test
@DisplayName("subject가 없는 서명 JWT는 bad_credentials로 변환된다")
fun shouldRejectSignedJwtWithoutSubjectAsBadCredentials() {
val token = signedToken(subject = null)
val exception = assertThrows(SodaException::class.java) {
tokenProvider.getAuthentication(token)
}
assertEquals("common.error.bad_credentials", exception.messageKey)
}
@Test
@DisplayName("숫자가 아닌 subject의 서명 JWT는 bad_credentials로 변환된다")
fun shouldRejectSignedJwtWithNonNumericSubjectAsBadCredentials() {
val token = signedToken(subject = "not-a-number")
val exception = assertThrows(SodaException::class.java) {
tokenProvider.getAuthentication(token)
}
assertEquals("common.error.bad_credentials", exception.messageKey)
}
@Test
@DisplayName("auth claim이 없는 서명 JWT는 bad_credentials로 변환된다")
fun shouldRejectSignedJwtWithoutAuthoritiesClaimAsBadCredentials() {
val token = signedToken(subject = MEMBER_ID.toString(), authorities = null)
allowStoredToken(token)
val exception = assertThrows(SodaException::class.java) {
tokenProvider.getAuthentication(token)
}
assertEquals("common.error.bad_credentials", exception.messageKey)
}
@ParameterizedTest
@ValueSource(strings = ["", " ", "ROLE_ADMIN,", ",ROLE_ADMIN", "ROLE_ADMIN,,ROLE_USER"])
@DisplayName("비어 있는 authority를 포함한 서명 JWT는 bad_credentials로 변환된다")
fun shouldRejectSignedJwtWithBlankAuthorityAsBadCredentials(authorities: String) {
val token = signedToken(subject = MEMBER_ID.toString(), authorities = authorities)
allowStoredToken(token)
val exception = assertThrows(SodaException::class.java) {
tokenProvider.getAuthentication(token)
}
assertEquals("common.error.bad_credentials", exception.messageKey)
}
@Test
@DisplayName("문자열이 아닌 auth claim의 서명 JWT는 bad_credentials로 변환된다")
fun shouldRejectSignedJwtWithNonStringAuthoritiesClaimAsBadCredentials() {
val token = signedToken(subject = MEMBER_ID.toString(), authorities = 1)
allowStoredToken(token)
val exception = assertThrows(SodaException::class.java) {
tokenProvider.getAuthentication(token)
}
assertEquals("common.error.bad_credentials", exception.messageKey)
}
private fun allowStoredToken(token: String) {
val member = Member(
email = "admin@example.com",
password = "password",
nickname = "admin",
role = MemberRole.ADMIN
).apply { id = MEMBER_ID }
Mockito.`when`(tokenRepository.findById(MEMBER_ID))
.thenReturn(Optional.of(MemberToken(MEMBER_ID, mutableSetOf(token))))
Mockito.`when`(memberRepository.findById(MEMBER_ID)).thenReturn(Optional.of(member))
}
private fun signedToken(subject: String?, authorities: Any? = "ROLE_ADMIN"): String {
val builder = Jwts.builder()
.signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET)), SignatureAlgorithm.HS512)
.setExpiration(Date(System.currentTimeMillis() + 60_000))
if (subject != null) {
builder.setSubject(subject)
}
if (authorities != null) {
builder.claim("auth", authorities)
}
return builder.compact()
}
companion object {
private const val MEMBER_ID = 1L
private val SECRET = Base64.getEncoder().encodeToString("a".repeat(64).toByteArray())
}
}