test #433
@@ -31,6 +31,7 @@ import kr.co.vividnext.sodalive.member.Member
|
|||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
import kr.co.vividnext.sodalive.member.MemberService
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkResponse
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
@@ -579,7 +580,7 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun writeCheers(request: PostWriteCheersRequest, member: Member) {
|
fun writeCheers(request: PostWriteCheersRequest, member: Member): CreatorChannelFanTalkResponse {
|
||||||
val creator = queryRepository.getMember(request.creatorId)
|
val creator = queryRepository.getMember(request.creatorId)
|
||||||
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
||||||
|
|
||||||
@@ -605,18 +606,20 @@ class ExplorerService(
|
|||||||
cheers.parent = parent
|
cheers.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
cheersRepository.save(cheers)
|
val savedCheers = cheersRepository.save(cheers)
|
||||||
|
|
||||||
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
if (request.languageCode.isNullOrBlank()) {
|
if (request.languageCode.isNullOrBlank()) {
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
LanguageDetectEvent(
|
LanguageDetectEvent(
|
||||||
id = cheers.id!!,
|
id = savedCheers.id!!,
|
||||||
query = request.content,
|
query = request.content,
|
||||||
targetType = LanguageDetectTargetType.CREATOR_CHEERS
|
targetType = LanguageDetectTargetType.CREATOR_CHEERS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CreatorChannelFanTalkResponse.from(savedCheers, cloudFrontHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCreatorProfileCheers(
|
fun getCreatorProfileCheers(
|
||||||
@@ -639,24 +642,29 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun modifyCheers(request: PutWriteCheersRequest, member: Member) {
|
fun modifyCheers(request: PutWriteCheersRequest, member: Member): CreatorChannelFanTalkResponse {
|
||||||
val cheers = queryRepository.getCheers(request.cheersId)
|
val cheers = queryRepository.getCheers(request.cheersId)
|
||||||
?: throw SodaException(messageKey = "common.error.invalid_request")
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
val isWriter = cheers.member!!.id!! == member.id!!
|
||||||
|
val isCreator = cheers.creator!!.id!! == member.id!!
|
||||||
|
|
||||||
if (cheers.member!!.id!! == member.id!!) {
|
if (!isWriter && !isCreator) {
|
||||||
|
throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWriter) {
|
||||||
if (request.content != null) {
|
if (request.content != null) {
|
||||||
cheers.cheers = request.content
|
cheers.cheers = request.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isCreator || isWriter) {
|
||||||
cheers.creator!!.id!! == member.id!! ||
|
|
||||||
cheers.member!!.id!! == member.id!!
|
|
||||||
) {
|
|
||||||
if (request.isActive != null) {
|
if (request.isActive != null) {
|
||||||
cheers.isActive = request.isActive
|
cheers.isActive = request.isActive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CreatorChannelFanTalkResponse.from(cheers, cloudFrontHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
|
||||||
|
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
|
||||||
import kr.co.vividnext.sodalive.extensions.toUtcIso
|
import kr.co.vividnext.sodalive.extensions.toUtcIso
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalk
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalk
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkReply
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkReply
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
||||||
@@ -48,6 +51,20 @@ data class CreatorChannelFanTalkResponse(
|
|||||||
creatorReplies = fanTalk.creatorReplies.map(CreatorChannelFanTalkReplyResponse::from)
|
creatorReplies = fanTalk.creatorReplies.map(CreatorChannelFanTalkReplyResponse::from)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun from(fanTalk: CreatorCheers, cloudFrontHost: String): CreatorChannelFanTalkResponse {
|
||||||
|
val writer = fanTalk.member!!
|
||||||
|
return CreatorChannelFanTalkResponse(
|
||||||
|
fanTalkId = fanTalk.id!!,
|
||||||
|
writerId = writer.id!!,
|
||||||
|
writerNickname = writer.nickname.removeDeletedNicknamePrefix(),
|
||||||
|
writerProfileImageUrl = writer.profileImage.toCdnUrl(cloudFrontHost)
|
||||||
|
?: "$cloudFrontHost/profile/default-profile.png",
|
||||||
|
content = fanTalk.cheers,
|
||||||
|
createdAtUtc = fanTalk.createdAt!!.toUtcIso(),
|
||||||
|
creatorReplies = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import kr.co.vividnext.sodalive.common.CountryContext
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
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.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkResponse
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||||
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
@WebMvcTest(ExplorerController::class)
|
||||||
|
@Import(ExplorerControllerTest.TestSecurityConfig::class)
|
||||||
|
class ExplorerControllerTest @Autowired constructor(
|
||||||
|
private val mockMvc: MockMvc,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
|
) {
|
||||||
|
@MockBean
|
||||||
|
private lateinit var service: ExplorerService
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var messageSource: SodaMessageSource
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var langContext: LangContext
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var countryContext: CountryContext
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
class TestSecurityConfig {
|
||||||
|
@Bean
|
||||||
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
return http
|
||||||
|
.csrf().disable()
|
||||||
|
.authorizeRequests()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
.and()
|
||||||
|
.exceptionHandling()
|
||||||
|
.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||||
|
.accessDeniedHandler { _, response, _ -> response.sendError(HttpServletResponse.SC_FORBIDDEN) }
|
||||||
|
.and()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("팬톡 작성 API는 저장된 팬톡 데이터를 data에 반환한다")
|
||||||
|
fun shouldReturnCreatedFanTalkData() {
|
||||||
|
val member = createMember(id = 10L)
|
||||||
|
val request = PostWriteCheersRequest(creatorId = 1L, content = "created fan talk", languageCode = "ko")
|
||||||
|
Mockito.doReturn(createFanTalkResponse(content = "created fan talk"))
|
||||||
|
.`when`(service).writeCheers(eqValue(request), eqValue(member))
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
post("/explorer/profile/cheers")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(request))
|
||||||
|
.with(user(MemberAdapter(member)))
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.fanTalkId").value(101))
|
||||||
|
.andExpect(jsonPath("$.data.writerId").value(10))
|
||||||
|
.andExpect(jsonPath("$.data.writerNickname").value("fan"))
|
||||||
|
.andExpect(jsonPath("$.data.writerProfileImageUrl").value("https://cdn.test/profile/fan.png"))
|
||||||
|
.andExpect(jsonPath("$.data.content").value("created fan talk"))
|
||||||
|
.andExpect(jsonPath("$.data.createdAtUtc").value("2026-07-09T01:00:00Z"))
|
||||||
|
.andExpect(jsonPath("$.data.creatorReplies").isArray)
|
||||||
|
.andExpect(jsonPath("$.data.creatorReplies.length()").value(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("팬톡 수정 API는 수정된 팬톡 데이터를 data에 반환한다")
|
||||||
|
fun shouldReturnModifiedFanTalkData() {
|
||||||
|
val member = createMember(id = 10L)
|
||||||
|
val request = PutWriteCheersRequest(cheersId = 101L, content = "modified fan talk")
|
||||||
|
Mockito.doReturn(createFanTalkResponse(content = "modified fan talk"))
|
||||||
|
.`when`(service).modifyCheers(eqValue(request), eqValue(member))
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
put("/explorer/profile/cheers")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(request))
|
||||||
|
.with(user(MemberAdapter(member)))
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.fanTalkId").value(101))
|
||||||
|
.andExpect(jsonPath("$.data.content").value("modified fan talk"))
|
||||||
|
.andExpect(jsonPath("$.data.creatorReplies.length()").value(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(id: Long): Member {
|
||||||
|
return Member(
|
||||||
|
email = "user$id@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "user$id",
|
||||||
|
role = MemberRole.USER
|
||||||
|
).apply { this.id = id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFanTalkResponse(content: String): CreatorChannelFanTalkResponse {
|
||||||
|
return CreatorChannelFanTalkResponse(
|
||||||
|
fanTalkId = 101L,
|
||||||
|
writerId = 10L,
|
||||||
|
writerNickname = "fan",
|
||||||
|
writerProfileImageUrl = "https://cdn.test/profile/fan.png",
|
||||||
|
content = content,
|
||||||
|
createdAtUtc = "2026-07-09T01:00:00Z",
|
||||||
|
creatorReplies = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> eqValue(value: T): T {
|
||||||
|
return Mockito.eq(value) ?: value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.content.AudioContentService
|
||||||
|
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
||||||
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.ChannelNoticeRepository
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class ExplorerServiceTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("팬톡 수정은 작성자나 크리에이터가 아닌 요청자에게 데이터를 반환하지 않는다")
|
||||||
|
fun shouldRejectModifyCheersForNonWriterAndNonCreator() {
|
||||||
|
val queryRepository = Mockito.mock(ExplorerQueryRepository::class.java)
|
||||||
|
val service = createService(queryRepository)
|
||||||
|
val cheers = createCheers(
|
||||||
|
writer = createMember(id = 10L, nickname = "writer"),
|
||||||
|
creator = createMember(id = 20L, nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
)
|
||||||
|
val requester = createMember(id = 30L, nickname = "requester")
|
||||||
|
Mockito.doReturn(cheers).`when`(queryRepository).getCheers(101L)
|
||||||
|
|
||||||
|
assertThrows(SodaException::class.java) {
|
||||||
|
service.modifyCheers(PutWriteCheersRequest(cheersId = 101L, content = "stolen"), requester)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createService(queryRepository: ExplorerQueryRepository): ExplorerService {
|
||||||
|
return ExplorerService(
|
||||||
|
memberService = Mockito.mock(MemberService::class.java),
|
||||||
|
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
|
||||||
|
audioContentService = Mockito.mock(AudioContentService::class.java),
|
||||||
|
donationRankingService = Mockito.mock(CreatorDonationRankingService::class.java),
|
||||||
|
queryRepository = queryRepository,
|
||||||
|
cheersRepository = Mockito.mock(CreatorCheersRepository::class.java),
|
||||||
|
noticeRepository = Mockito.mock(ChannelNoticeRepository::class.java),
|
||||||
|
channelDonationService = Mockito.mock(ChannelDonationService::class.java),
|
||||||
|
communityService = Mockito.mock(CreatorCommunityService::class.java),
|
||||||
|
seriesService = Mockito.mock(ContentSeriesService::class.java),
|
||||||
|
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java),
|
||||||
|
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
|
||||||
|
messageSource = Mockito.mock(SodaMessageSource::class.java),
|
||||||
|
langContext = Mockito.mock(LangContext::class.java),
|
||||||
|
cloudFrontHost = "https://cdn.test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCheers(writer: Member, creator: Member): CreatorCheers {
|
||||||
|
return CreatorCheers(cheers = "fan talk", languageCode = "ko").apply {
|
||||||
|
id = 101L
|
||||||
|
member = writer
|
||||||
|
this.creator = creator
|
||||||
|
createdAt = LocalDateTime.of(2026, 7, 9, 1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(
|
||||||
|
id: Long,
|
||||||
|
nickname: String,
|
||||||
|
role: MemberRole = MemberRole.USER
|
||||||
|
): Member {
|
||||||
|
return Member(
|
||||||
|
email = "$nickname@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = nickname,
|
||||||
|
role = role
|
||||||
|
).apply { this.id = id }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user