feat(fantalk): 팬톡 작성 수정 응답을 반환한다

This commit is contained in:
2026-07-09 06:00:35 +09:00
parent ae380e8922
commit b5b682e340
4 changed files with 261 additions and 9 deletions
@@ -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 }
}
}