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 shouldThrowAdultVerificationRequiredWhenAdultContentRequestedByNonAdultPolicy() { val viewer = createMember(id = 1002L, nickname = "viewer") val creator = createMember(id = 2002L, nickname = "creator") val adultContent = createAudioContent(creator = creator, isAdult = true) Mockito.`when`(repository.findById(adultContent.id!!)).thenReturn(Optional.of(adultContent)) val exception = assertThrows(SodaException::class.java) { service.getDetail( id = adultContent.id!!, member = viewer, isAdultContentVisible = false, timezone = "Asia/Seoul" ) } assertEquals("common.error.adult_verification_required", exception.messageKey) Mockito.verifyNoInteractions(explorerQueryRepository) } @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, isAdult: Boolean = false): 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 = isAdult, 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 } }