fix(content): 차단된 구매자의 오디오 상세 조회를 허용한다

This commit is contained in:
2026-03-24 19:21:58 +09:00
parent 681ee11784
commit 447735cad5
3 changed files with 307 additions and 9 deletions

View File

@@ -0,0 +1,250 @@
package kr.co.vividnext.sodalive.content
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository
import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository
import kr.co.vividnext.sodalive.content.order.LimitedEditionOrderRepository
import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.context.ApplicationEventPublisher
import java.util.Optional
class AudioContentServiceTest {
private lateinit var repository: AudioContentRepository
private lateinit var explorerQueryRepository: ExplorerQueryRepository
private lateinit var blockMemberRepository: BlockMemberRepository
private lateinit var hashTagRepository: HashTagRepository
private lateinit var orderRepository: OrderRepository
private lateinit var limitedEditionOrderRepository: LimitedEditionOrderRepository
private lateinit var themeQueryRepository: AudioContentThemeQueryRepository
private lateinit var playbackTrackingRepository: PlaybackTrackingRepository
private lateinit var commentRepository: AudioContentCommentRepository
private lateinit var audioContentLikeRepository: AudioContentLikeRepository
private lateinit var pinContentRepository: PinContentRepository
private lateinit var translationService: PapagoTranslationService
private lateinit var contentTranslationRepository: ContentTranslationRepository
private lateinit var s3Uploader: S3Uploader
private lateinit var audioContentCloudFront: AudioContentCloudFront
private lateinit var applicationEventPublisher: ApplicationEventPublisher
private lateinit var contentThemeTranslationRepository: ContentThemeTranslationRepository
private lateinit var service: AudioContentService
@BeforeEach
fun setUp() {
repository = Mockito.mock(AudioContentRepository::class.java)
explorerQueryRepository = Mockito.mock(ExplorerQueryRepository::class.java)
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java)
hashTagRepository = Mockito.mock(HashTagRepository::class.java)
orderRepository = Mockito.mock(OrderRepository::class.java)
limitedEditionOrderRepository = Mockito.mock(LimitedEditionOrderRepository::class.java)
themeQueryRepository = Mockito.mock(AudioContentThemeQueryRepository::class.java)
playbackTrackingRepository = Mockito.mock(PlaybackTrackingRepository::class.java)
commentRepository = Mockito.mock(AudioContentCommentRepository::class.java)
audioContentLikeRepository = Mockito.mock(AudioContentLikeRepository::class.java)
pinContentRepository = Mockito.mock(PinContentRepository::class.java)
translationService = Mockito.mock(PapagoTranslationService::class.java)
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java)
s3Uploader = Mockito.mock(S3Uploader::class.java)
audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java)
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
contentThemeTranslationRepository = Mockito.mock(ContentThemeTranslationRepository::class.java)
service = AudioContentService(
repository = repository,
explorerQueryRepository = explorerQueryRepository,
blockMemberRepository = blockMemberRepository,
hashTagRepository = hashTagRepository,
orderRepository = orderRepository,
limitedEditionOrderRepository = limitedEditionOrderRepository,
themeQueryRepository = themeQueryRepository,
playbackTrackingRepository = playbackTrackingRepository,
commentRepository = commentRepository,
audioContentLikeRepository = audioContentLikeRepository,
pinContentRepository = pinContentRepository,
translationService = translationService,
contentTranslationRepository = contentTranslationRepository,
s3Uploader = s3Uploader,
objectMapper = ObjectMapper(),
audioContentCloudFront = audioContentCloudFront,
applicationEventPublisher = applicationEventPublisher,
messageSource = SodaMessageSource(),
langContext = LangContext(),
contentThemeTranslationRepository = contentThemeTranslationRepository,
audioContentBucket = "audio-bucket",
coverImageBucket = "cover-bucket",
coverImageHost = "https://cdn.test"
)
}
@Test
@DisplayName("차단 + 미구매 사용자 요청은 콘텐츠 상세에서 차단 예외를 반환한다")
fun shouldThrowBlockedAccessWhenBlockedAndNotPurchased() {
val viewer = createMember(id = 1000L, nickname = "viewer")
val creator = createMember(id = 2000L, nickname = "creator")
val audioContent = createAudioContent(creator)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
Mockito.`when`(explorerQueryRepository.getMember(creator.id!!)).thenReturn(creator)
Mockito.`when`(
orderRepository.isExistOrderedAndOrderType(
memberId = viewer.id!!,
contentId = audioContent.id!!
)
).thenReturn(Pair(false, null))
Mockito.`when`(blockMemberRepository.isBlocked(blockedMemberId = viewer.id!!, memberId = creator.id!!))
.thenReturn(true)
val exception = assertThrows(SodaException::class.java) {
service.getDetail(
id = audioContent.id!!,
member = viewer,
isAdultContentVisible = false,
timezone = "Asia/Seoul"
)
}
assertEquals("content.error.blocked_access", exception.messageKey)
}
@Test
@DisplayName("차단 + 구매 사용자 요청은 상세 조회를 허용하고 댓글/이전다음 조회를 생략한다")
fun shouldAllowDetailWhenBlockedAndPurchasedButSkipCommentAndNavigationQueries() {
val viewer = createMember(id = 1001L, nickname = "viewer")
val creator = createMember(id = 2001L, nickname = "creator")
val audioContent = createAudioContent(creator)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
Mockito.`when`(explorerQueryRepository.getMember(creator.id!!)).thenReturn(creator)
Mockito.`when`(
orderRepository.isExistOrderedAndOrderType(
memberId = viewer.id!!,
contentId = audioContent.id!!
)
).thenReturn(Pair(true, OrderType.KEEP))
Mockito.`when`(blockMemberRepository.isBlocked(blockedMemberId = viewer.id!!, memberId = creator.id!!))
.thenReturn(true)
Mockito.`when`(explorerQueryRepository.getCreatorFollowing(creator.id!!, viewer.id!!)).thenReturn(null)
Mockito.`when`(
limitedEditionOrderRepository.getOrderSequence(
contentId = audioContent.id!!,
memberId = viewer.id!!
)
).thenReturn(null)
Mockito.`when`(
audioContentCloudFront.generateSignedURL(
resourcePath = audioContent.content!!,
expirationTime = 7_200_000L
)
).thenReturn("https://signed.test/audio")
Mockito.`when`(
repository.getCreatorOtherContentList(
cloudfrontHost = "https://cdn.test",
contentId = audioContent.id!!,
creatorId = creator.id!!,
isAdult = false
)
).thenReturn(emptyList())
Mockito.`when`(
repository.getSameThemeOtherContentList(
cloudfrontHost = "https://cdn.test",
contentId = audioContent.id!!,
themeId = audioContent.theme!!.id!!,
isAdult = false
)
).thenReturn(emptyList())
Mockito.`when`(audioContentLikeRepository.totalCountAudioContentLike(audioContent.id!!)).thenReturn(0)
Mockito.`when`(audioContentLikeRepository.findByMemberIdAndContentId(viewer.id!!, audioContent.id!!)).thenReturn(null)
Mockito.`when`(
pinContentRepository.findByContentIdAndMemberId(
contentId = audioContent.id!!,
memberId = viewer.id!!,
active = true
)
).thenReturn(null)
Mockito.`when`(pinContentRepository.getPinContentList(memberId = viewer.id!!, active = true)).thenReturn(emptyList())
Mockito.`when`(
contentThemeTranslationRepository.findByContentThemeIdAndLocale(
contentThemeId = audioContent.theme!!.id!!,
locale = "ko"
)
).thenReturn(null)
val response = service.getDetail(
id = audioContent.id!!,
member = viewer,
isAdultContentVisible = false,
timezone = "Asia/Seoul"
)
assertTrue(response.existOrdered)
assertTrue(response.commentList.isEmpty())
assertEquals(0, response.commentCount)
assertNull(response.previousContent)
assertNull(response.nextContent)
Mockito.verify(repository, Mockito.never()).findSeriesIdByContentId(audioContent.id!!, false)
Mockito.verifyNoInteractions(commentRepository)
}
private fun createMember(id: Long, nickname: String): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname
)
member.id = id
return member
}
private fun createAudioContent(creator: Member): AudioContent {
val theme = AudioContentTheme(theme = "수면", image = "sleep.png")
theme.id = 300L
val audioContent = AudioContent(
title = "테스트 제목",
detail = "테스트 상세 설명",
languageCode = null,
price = 100,
purchaseOption = PurchaseOption.BOTH,
isGeneratePreview = true,
isOnlyRental = false,
isAdult = false,
isPointAvailable = true,
isCommentAvailable = true,
isFullDetailVisible = true
)
audioContent.id = 500L
audioContent.member = creator
audioContent.theme = theme
audioContent.content = "output/500/content.mp3"
audioContent.coverImage = "audio_content_cover/500/cover.jpg"
audioContent.duration = "00:10:00"
audioContent.isActive = true
return audioContent
}
}