revert(ai-character): 기존 관리자 기능 기반을 제거한다

This commit is contained in:
2026-07-23 16:47:00 +09:00
parent a0bf71be47
commit d3564f8c0d
40 changed files with 83 additions and 11634 deletions
@@ -1,162 +0,0 @@
package kr.co.vividnext.sodalive.content
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler
import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint
import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import org.hamcrest.Matchers.anEmptyMap
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Import
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
@WebMvcTest(AudioContentController::class)
@Import(SecurityConfig::class, JwtAuthenticationEntryPoint::class, JwtAccessDeniedHandler::class)
class AudioContentUploadCompletionContractTest @Autowired constructor(
private val mockMvc: MockMvc
) {
@MockBean
private lateinit var service: AudioContentService
@MockBean
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
@MockBean
private lateinit var tokenProvider: TokenProvider
@MockBean
private lateinit var countryContext: CountryContext
@MockBean
private lateinit var langContext: LangContext
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@Test
@DisplayName("upload-complete callback은 기존 PUT 경로와 ADMIN/BOT 권한 계약을 유지한다")
fun shouldKeepUploadCompleteRouteAndRoles() {
val classMapping = AudioContentController::class.java.getAnnotation(RequestMapping::class.java)
val method = AudioContentController::class.java.getDeclaredMethod(
"uploadComplete",
UploadCompleteRequest::class.java,
kr.co.vividnext.sodalive.member.Member::class.java
)
val putMapping = method.getAnnotation(PutMapping::class.java)
val preAuthorize = method.getAnnotation(PreAuthorize::class.java)
assertEquals("/audio-content", classMapping.value.single())
assertEquals("/upload-complete", putMapping.value.single())
assertEquals("hasAnyRole('ADMIN', 'BOT')", preAuthorize.value)
}
@Test
@DisplayName("upload-complete request는 기존 contentId/contentPath/duration field를 유지한다")
fun shouldKeepUploadCompleteRequestFields() {
val request = UploadCompleteRequest(contentId = 1L, contentPath = "1/output.mp3", duration = "00:01:00")
assertEquals(1L, request.contentId)
assertEquals("1/output.mp3", request.contentPath)
assertEquals("00:01:00", request.duration)
}
@Test
@DisplayName("upload-complete callback은 기존 성공 응답 surface를 유지한다")
fun shouldKeepUploadCompleteSuccessResponseSurface() {
givenToken("bot-token", MemberRole.BOT)
mockMvc.perform(
put("/audio-content/upload-complete")
.header("Authorization", "Bearer bot-token")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"contentId":1,"contentPath":"1/output.mp3","duration":"00:01:00"}""")
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data").value(anEmptyMap<Any, Any>()))
.andExpect(content().json(LEGACY_UPLOAD_COMPLETE_SUCCESS_RESPONSE, true))
verify(service).uploadComplete(1L, "1/output.mp3", "00:01:00")
}
@Test
@DisplayName("upload-complete callback은 ADMIN/BOT JWT만 허용하고 USER/invalid JWT/익명 요청은 거부한다")
fun shouldAuthorizeUploadCompleteWithProductionSecurityChain() {
givenToken("admin-token", MemberRole.ADMIN)
givenToken("bot-token", MemberRole.BOT)
givenToken("user-token", MemberRole.USER)
Mockito.`when`(tokenProvider.validateToken("invalid-token")).thenReturn(false)
mockMvc.perform(uploadCompleteRequest("user-token"))
.andExpect(status().isForbidden)
mockMvc.perform(uploadCompleteRequest("invalid-token"))
.andExpect(status().isUnauthorized)
mockMvc.perform(uploadCompleteRequest("admin-token"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
mockMvc.perform(uploadCompleteRequest("bot-token"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
mockMvc.perform(
put("/audio-content/upload-complete")
.contentType(MediaType.APPLICATION_JSON)
.content(UPLOAD_COMPLETE_REQUEST)
)
.andExpect(status().isUnauthorized)
}
private fun uploadCompleteRequest(token: String) = put("/audio-content/upload-complete")
.header("Authorization", "Bearer $token")
.contentType(MediaType.APPLICATION_JSON)
.content(UPLOAD_COMPLETE_REQUEST)
private fun givenToken(token: String, role: MemberRole) {
val member = Member(
email = "${role.name.lowercase()}@test.com",
password = "password",
nickname = role.name.lowercase(),
role = role
).apply { id = role.ordinal.toLong() + 1 }
val authentication = UsernamePasswordAuthenticationToken(
MemberAdapter(member),
token,
MemberAdapter(member).authorities
)
Mockito.`when`(tokenProvider.validateToken(token)).thenReturn(true)
Mockito.`when`(tokenProvider.getAuthentication(token)).thenReturn(authentication)
}
companion object {
private const val UPLOAD_COMPLETE_REQUEST =
"""{"contentId":1,"contentPath":"1/output.mp3","duration":"00:01:00"}"""
private const val LEGACY_UPLOAD_COMPLETE_SUCCESS_RESPONSE =
"""{"success":true,"message":null,"data":{},"errorProperty":null}"""
}
}