feat(content-preference): 콘텐츠 조회 설정 서버 저장 전환을 반영한다
This commit is contained in:
@@ -100,6 +100,28 @@ class AudioContentServiceTest {
|
||||
)
|
||||
}
|
||||
|
||||
@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() {
|
||||
@@ -220,7 +242,7 @@ class AudioContentServiceTest {
|
||||
return member
|
||||
}
|
||||
|
||||
private fun createAudioContent(creator: Member): AudioContent {
|
||||
private fun createAudioContent(creator: Member, isAdult: Boolean = false): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "수면", image = "sleep.png")
|
||||
theme.id = 300L
|
||||
|
||||
@@ -232,7 +254,7 @@ class AudioContentServiceTest {
|
||||
purchaseOption = PurchaseOption.BOTH,
|
||||
isGeneratePreview = true,
|
||||
isOnlyRental = false,
|
||||
isAdult = false,
|
||||
isAdult = isAdult,
|
||||
isPointAvailable = true,
|
||||
isCommentAvailable = true,
|
||||
isFullDetailVisible = true
|
||||
|
||||
@@ -8,7 +8,9 @@ import kr.co.vividnext.sodalive.can.use.UseCanRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityCommentRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLikeRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.PostCommunityPostLikeRequest
|
||||
import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue
|
||||
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
||||
import kr.co.vividnext.sodalive.fcm.FcmEventType
|
||||
@@ -36,6 +38,7 @@ import java.util.Optional
|
||||
class CreatorCommunityServiceTest {
|
||||
private lateinit var repository: CreatorCommunityRepository
|
||||
private lateinit var blockMemberRepository: BlockMemberRepository
|
||||
private lateinit var likeRepository: CreatorCommunityLikeRepository
|
||||
private lateinit var commentRepository: CreatorCommunityCommentRepository
|
||||
private lateinit var useCanRepository: UseCanRepository
|
||||
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||
@@ -45,6 +48,7 @@ class CreatorCommunityServiceTest {
|
||||
fun setup() {
|
||||
repository = Mockito.mock(CreatorCommunityRepository::class.java)
|
||||
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java)
|
||||
likeRepository = Mockito.mock(CreatorCommunityLikeRepository::class.java)
|
||||
commentRepository = Mockito.mock(CreatorCommunityCommentRepository::class.java)
|
||||
useCanRepository = Mockito.mock(UseCanRepository::class.java)
|
||||
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
@@ -53,7 +57,7 @@ class CreatorCommunityServiceTest {
|
||||
canPaymentService = Mockito.mock(CanPaymentService::class.java),
|
||||
repository = repository,
|
||||
blockMemberRepository = blockMemberRepository,
|
||||
likeRepository = Mockito.mock(CreatorCommunityLikeRepository::class.java),
|
||||
likeRepository = likeRepository,
|
||||
commentRepository = commentRepository,
|
||||
useCanRepository = useCanRepository,
|
||||
s3Uploader = Mockito.mock(S3Uploader::class.java),
|
||||
@@ -68,6 +72,29 @@ class CreatorCommunityServiceTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("좋아요 처리 시 전달된 성인 여부를 기준으로 게시글을 조회한다")
|
||||
fun shouldUseProvidedIsAdultForCommunityLikeAdultFilter() {
|
||||
val member = createMember(id = 88L, role = MemberRole.USER, nickname = "viewer")
|
||||
val post = CreatorCommunity(content = "adult-post", price = 0, isCommentAvailable = true, isAdult = true)
|
||||
post.id = 801L
|
||||
post.member = createMember(id = 99L, role = MemberRole.CREATOR, nickname = "creator")
|
||||
|
||||
Mockito.`when`(likeRepository.findByPostIdAndMemberId(postId = 801L, memberId = 88L)).thenReturn(null)
|
||||
Mockito.`when`(repository.findByIdAndActive(801L, true)).thenReturn(post)
|
||||
Mockito.`when`(likeRepository.save(Mockito.any(CreatorCommunityLike::class.java)))
|
||||
.thenAnswer { invocation -> invocation.getArgument(0) }
|
||||
|
||||
val response = service.communityPostLike(
|
||||
request = PostCommunityPostLikeRequest(postId = 801L),
|
||||
member = member,
|
||||
isAdult = true
|
||||
)
|
||||
|
||||
assertTrue(response.like)
|
||||
Mockito.verify(repository).findByIdAndActive(801L, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터가 아닌 사용자가 댓글 작성 시 크리에이터 대상 커뮤니티 딥링크 알림 이벤트를 발행한다")
|
||||
fun shouldPublishCreatorCommunityCommentNotificationEventWhenCommenterIsNotCreator() {
|
||||
@@ -77,7 +104,7 @@ class CreatorCommunityServiceTest {
|
||||
post.id = 301L
|
||||
post.member = creator
|
||||
|
||||
Mockito.`when`(repository.findById(post.id!!)).thenReturn(Optional.of(post))
|
||||
Mockito.`when`(repository.findByIdAndActive(post.id!!, true)).thenReturn(post)
|
||||
Mockito.`when`(useCanRepository.isExistCommunityPostOrdered(post.id!!, commenter.id!!)).thenReturn(false)
|
||||
Mockito.`when`(commentRepository.save(Mockito.any(CreatorCommunityComment::class.java)))
|
||||
.thenAnswer { invocation -> invocation.getArgument(0) }
|
||||
@@ -87,7 +114,8 @@ class CreatorCommunityServiceTest {
|
||||
comment = "새 댓글",
|
||||
postId = post.id!!,
|
||||
parentId = null,
|
||||
isSecret = false
|
||||
isSecret = false,
|
||||
isAdult = true
|
||||
)
|
||||
|
||||
val captor = ArgumentCaptor.forClass(FcmEvent::class.java)
|
||||
@@ -112,7 +140,7 @@ class CreatorCommunityServiceTest {
|
||||
post.id = 401L
|
||||
post.member = creator
|
||||
|
||||
Mockito.`when`(repository.findById(post.id!!)).thenReturn(Optional.of(post))
|
||||
Mockito.`when`(repository.findByIdAndActive(post.id!!, true)).thenReturn(post)
|
||||
Mockito.`when`(useCanRepository.isExistCommunityPostOrdered(post.id!!, creator.id!!)).thenReturn(false)
|
||||
Mockito.`when`(commentRepository.save(Mockito.any(CreatorCommunityComment::class.java)))
|
||||
.thenAnswer { invocation -> invocation.getArgument(0) }
|
||||
@@ -122,12 +150,80 @@ class CreatorCommunityServiceTest {
|
||||
comment = "내가 단 댓글",
|
||||
postId = post.id!!,
|
||||
parentId = null,
|
||||
isSecret = false
|
||||
isSecret = false,
|
||||
isAdult = true
|
||||
)
|
||||
|
||||
Mockito.verify(applicationEventPublisher, Mockito.never()).publishEvent(Mockito.any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비성인 정책 사용자가 성인 커뮤니티 게시글에 댓글 작성 시 예외가 발생한다")
|
||||
fun shouldThrowExceptionWhenCommentingAdultPostWithNonAdultPolicy() {
|
||||
val commenter = createMember(id = 23L, role = MemberRole.USER, nickname = "viewer")
|
||||
|
||||
Mockito.`when`(repository.findByIdAndActive(901L, false)).thenReturn(null)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.createCommunityPostComment(
|
||||
member = commenter,
|
||||
comment = "접근 불가 댓글",
|
||||
postId = 901L,
|
||||
isAdult = false
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("creator.community.invalid_request_retry", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비성인 정책 사용자가 성인 게시글 댓글 목록을 조회하면 예외가 발생한다")
|
||||
fun shouldThrowExceptionWhenFetchingAdultPostCommentsWithNonAdultPolicy() {
|
||||
Mockito.`when`(repository.findByIdAndActive(902L, false)).thenReturn(null)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.getCommunityPostCommentList(
|
||||
postId = 902L,
|
||||
memberId = 23L,
|
||||
timezone = "Asia/Seoul",
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
isAdult = false
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("creator.community.invalid_request_retry", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비성인 정책 사용자가 성인 게시글 댓글의 답글 목록을 조회하면 예외가 발생한다")
|
||||
fun shouldThrowExceptionWhenFetchingReplyOfAdultPostWithNonAdultPolicy() {
|
||||
val creator = createMember(id = 31L, role = MemberRole.CREATOR, nickname = "creator")
|
||||
val adultPost = CreatorCommunity(content = "adult-post", price = 0, isCommentAvailable = true, isAdult = true)
|
||||
adultPost.id = 903L
|
||||
adultPost.member = creator
|
||||
|
||||
val parentComment = CreatorCommunityComment(comment = "parent", isSecret = false)
|
||||
parentComment.id = 1001L
|
||||
parentComment.creatorCommunity = adultPost
|
||||
parentComment.member = creator
|
||||
|
||||
Mockito.`when`(commentRepository.findById(1001L)).thenReturn(Optional.of(parentComment))
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.getCommentReplyList(
|
||||
commentId = 1001L,
|
||||
memberId = 32L,
|
||||
timezone = "Asia/Seoul",
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
isAdult = false
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("creator.community.invalid_request_retry", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("고정 게시물이 이미 3개면 추가 고정 시 예외가 발생한다")
|
||||
fun shouldThrowExceptionWhenPinCountExceedsLimit() {
|
||||
|
||||
@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.live.recommend
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -11,13 +13,22 @@ import org.mockito.Mockito
|
||||
class LiveRecommendServiceTest {
|
||||
private lateinit var repository: LiveRecommendRepository
|
||||
private lateinit var blockMemberRepository: BlockMemberRepository
|
||||
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
|
||||
private lateinit var liveRecommendCacheService: LiveRecommendCacheService
|
||||
private lateinit var service: LiveRecommendService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = Mockito.mock(LiveRecommendRepository::class.java)
|
||||
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java)
|
||||
service = LiveRecommendService(repository, blockMemberRepository)
|
||||
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
liveRecommendCacheService = Mockito.mock(LiveRecommendCacheService::class.java)
|
||||
service = LiveRecommendService(
|
||||
repository = repository,
|
||||
blockMemberRepository = blockMemberRepository,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
liveRecommendCacheService = liveRecommendCacheService
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -39,24 +50,35 @@ class LiveRecommendServiceTest {
|
||||
auth.member = member
|
||||
|
||||
val expected = listOf(GetRecommendLiveResponse(imageUrl = "https://cdn.test/recommend.png", creatorId = 77L))
|
||||
Mockito.`when`(repository.getRecommendLive(memberId = member.id, isAdult = true)).thenReturn(expected)
|
||||
Mockito.`when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn(
|
||||
ViewerContentPreference(
|
||||
countryCode = "KR",
|
||||
isAdultContentVisible = true,
|
||||
contentType = kr.co.vividnext.sodalive.content.ContentType.ALL,
|
||||
isAdult = true
|
||||
)
|
||||
)
|
||||
Mockito.`when`(liveRecommendCacheService.getRecommendLive(memberId = member.id, isAdult = true)).thenReturn(expected)
|
||||
|
||||
val result = service.getRecommendLive(member)
|
||||
|
||||
assertEquals(expected, result)
|
||||
Mockito.verify(repository).getRecommendLive(memberId = member.id, isAdult = true)
|
||||
Mockito.verify(liveRecommendCacheService).getRecommendLive(memberId = member.id, isAdult = true)
|
||||
Mockito.verifyNoInteractions(repository)
|
||||
Mockito.verifyNoInteractions(blockMemberRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldDelegateToRepositoryAsGuestWhenMemberIsNull() {
|
||||
val expected = listOf(GetRecommendLiveResponse(imageUrl = "https://cdn.test/recommend-guest.png", creatorId = 88L))
|
||||
Mockito.`when`(repository.getRecommendLive(memberId = null, isAdult = false)).thenReturn(expected)
|
||||
Mockito.`when`(liveRecommendCacheService.getRecommendLive(memberId = null, isAdult = false)).thenReturn(expected)
|
||||
|
||||
val result = service.getRecommendLive(null)
|
||||
|
||||
assertEquals(expected, result)
|
||||
Mockito.verify(repository).getRecommendLive(memberId = null, isAdult = false)
|
||||
Mockito.verify(liveRecommendCacheService).getRecommendLive(memberId = null, isAdult = false)
|
||||
Mockito.verifyNoInteractions(repository)
|
||||
Mockito.verifyNoInteractions(blockMemberRepository)
|
||||
Mockito.verifyNoInteractions(memberContentPreferenceService)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package kr.co.vividnext.sodalive.live.tag
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class LiveTagServiceTest {
|
||||
private lateinit var repository: LiveTagRepository
|
||||
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
|
||||
private lateinit var service: LiveTagService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = mock()
|
||||
memberContentPreferenceService = mock()
|
||||
|
||||
service = LiveTagService(
|
||||
repository = repository,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
objectMapper = ObjectMapper(),
|
||||
s3Uploader = mock<S3Uploader>(),
|
||||
coverImageBucket = "bucket",
|
||||
cloudFrontHost = "https://cdn.test"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("일반 사용자는 저장된 성인 설정값으로 라이브 태그 필터를 적용한다")
|
||||
fun shouldApplyStoredPreferenceForNonAdminMember() {
|
||||
val member = createMember(id = 1L, role = MemberRole.USER)
|
||||
val expected = listOf(GetLiveTagResponse(1L, "일반", "https://cdn.test/live1.png", false))
|
||||
|
||||
Mockito.`when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn(
|
||||
ViewerContentPreference(
|
||||
countryCode = "KR",
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.ALL,
|
||||
isAdult = false
|
||||
)
|
||||
)
|
||||
Mockito.`when`(repository.getTags(isAdult = false, cloudFrontHost = "https://cdn.test")).thenReturn(expected)
|
||||
|
||||
val actual = service.getTags(member)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
Mockito.verify(repository).getTags(isAdult = false, cloudFrontHost = "https://cdn.test")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자는 저장 설정과 무관하게 성인 태그를 포함해 조회한다")
|
||||
fun shouldAllowAdultTagsForAdmin() {
|
||||
val admin = createMember(id = 2L, role = MemberRole.ADMIN)
|
||||
val expected = listOf(GetLiveTagResponse(2L, "성인", "https://cdn.test/live2.png", true))
|
||||
|
||||
Mockito.`when`(repository.getTags(isAdult = true, cloudFrontHost = "https://cdn.test")).thenReturn(expected)
|
||||
|
||||
val actual = service.getTags(admin)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
Mockito.verify(repository).getTags(isAdult = true, cloudFrontHost = "https://cdn.test")
|
||||
Mockito.verifyNoInteractions(memberContentPreferenceService)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long, role: MemberRole): Member {
|
||||
val member = Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id",
|
||||
role = role
|
||||
)
|
||||
member.id = id
|
||||
return member
|
||||
}
|
||||
|
||||
private inline fun <reified T> mock(): T {
|
||||
return Mockito.mock(T::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package kr.co.vividnext.sodalive.member
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.UpdateMemberContentPreferenceRequest
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
|
||||
import kr.co.vividnext.sodalive.member.social.SocialAuthServiceResolver
|
||||
import kr.co.vividnext.sodalive.useraction.UserActionService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
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
|
||||
|
||||
class MemberControllerTest {
|
||||
private lateinit var memberService: MemberService
|
||||
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
|
||||
private lateinit var socialAuthServiceResolver: SocialAuthServiceResolver
|
||||
private lateinit var trackingService: AdTrackingService
|
||||
private lateinit var userActionService: UserActionService
|
||||
private lateinit var controller: MemberController
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
memberService = mock()
|
||||
memberContentPreferenceService = mock()
|
||||
socialAuthServiceResolver = mock()
|
||||
trackingService = mock()
|
||||
userActionService = mock()
|
||||
|
||||
controller = MemberController(
|
||||
service = memberService,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
socialAuthServiceResolver = socialAuthServiceResolver,
|
||||
trackingService = trackingService,
|
||||
userActionService = userActionService,
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PATCH /member/content-preference는 저장된 최신 설정을 응답한다")
|
||||
fun shouldReturnUpdatedPreferenceWhenRequestIsValid() {
|
||||
val member = createMember(1L)
|
||||
val request = UpdateMemberContentPreferenceRequest(
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
val viewerPreference = ViewerContentPreference(
|
||||
countryCode = "KR",
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE,
|
||||
isAdult = true
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
memberContentPreferenceService.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
).thenReturn(viewerPreference)
|
||||
|
||||
val response = controller.updateContentPreference(request, member)
|
||||
|
||||
assertTrue(response.success)
|
||||
assertEquals(true, response.data?.isAdultContentVisible)
|
||||
assertEquals(ContentType.FEMALE, response.data?.contentType)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비로그인 사용자는 PATCH /member/content-preference 호출 시 예외가 발생한다")
|
||||
fun shouldThrowWhenMemberIsNullOnUpdateContentPreference() {
|
||||
val request = UpdateMemberContentPreferenceRequest(
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
controller.updateContentPreference(request, null)
|
||||
}
|
||||
|
||||
assertEquals("common.error.bad_credentials", exception.messageKey)
|
||||
Mockito.verifyNoInteractions(memberContentPreferenceService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("두 필드 모두 누락된 PATCH 요청은 서비스 예외를 그대로 전파한다")
|
||||
fun shouldPropagateServiceExceptionWhenBothFieldsAreMissing() {
|
||||
val member = createMember(2L)
|
||||
val request = UpdateMemberContentPreferenceRequest(
|
||||
isAdultContentVisible = null,
|
||||
contentType = null
|
||||
)
|
||||
Mockito.`when`(
|
||||
memberContentPreferenceService.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = null,
|
||||
contentType = null
|
||||
)
|
||||
).thenThrow(SodaException(messageKey = "common.error.invalid_request"))
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
controller.updateContentPreference(request, member)
|
||||
}
|
||||
|
||||
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long): Member {
|
||||
val member = Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id"
|
||||
)
|
||||
member.id = id
|
||||
return member
|
||||
}
|
||||
|
||||
private inline fun <reified T> mock(): T {
|
||||
return Mockito.mock(T::class.java)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.member.auth.AuthRepository
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -42,7 +43,6 @@ class MemberServiceCacheEvictionTest {
|
||||
stipulationAgreeRepository = mock(),
|
||||
creatorFollowingRepository = mock(),
|
||||
blockMemberRepository = blockMemberRepository,
|
||||
authRepository = authRepository,
|
||||
signOutRepository = mock(),
|
||||
nicknameChangeLogRepository = mock(),
|
||||
memberTagRepository = mock(),
|
||||
@@ -63,6 +63,7 @@ class MemberServiceCacheEvictionTest {
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
countryContext = CountryContext(),
|
||||
memberContentPreferenceService = mock<MemberContentPreferenceService>(),
|
||||
objectMapper = ObjectMapper(),
|
||||
cacheManager = cacheManager,
|
||||
s3Bucket = "test-bucket",
|
||||
@@ -88,8 +89,8 @@ class MemberServiceCacheEvictionTest {
|
||||
|
||||
service.memberBlock(MemberBlockRequest(blockMemberId = blockedMemberId), memberId)
|
||||
|
||||
Mockito.verify(cache).evict("getRecommendLive:$memberId")
|
||||
Mockito.verify(cache).evict("getRecommendLive:$blockedMemberId")
|
||||
verifyRecommendLiveCacheEvicted(memberId)
|
||||
verifyRecommendLiveCacheEvicted(blockedMemberId)
|
||||
Mockito.verifyNoInteractions(authRepository)
|
||||
}
|
||||
|
||||
@@ -140,9 +141,9 @@ class MemberServiceCacheEvictionTest {
|
||||
blockedMemberId = linkedMemberId,
|
||||
memberId = memberId
|
||||
)
|
||||
Mockito.verify(cache).evict("getRecommendLive:$memberId")
|
||||
Mockito.verify(cache).evict("getRecommendLive:$blockedMemberId")
|
||||
Mockito.verify(cache, Mockito.never()).evict("getRecommendLive:$linkedMemberId")
|
||||
verifyRecommendLiveCacheEvicted(memberId)
|
||||
verifyRecommendLiveCacheEvicted(blockedMemberId)
|
||||
verifyRecommendLiveCacheNotEvicted(linkedMemberId)
|
||||
Mockito.verifyNoInteractions(authRepository)
|
||||
}
|
||||
|
||||
@@ -162,8 +163,20 @@ class MemberServiceCacheEvictionTest {
|
||||
service.memberUnBlock(MemberBlockRequest(blockMemberId = blockedMemberId), memberId)
|
||||
|
||||
assertEquals(false, blockMember.isActive)
|
||||
verifyRecommendLiveCacheEvicted(memberId)
|
||||
verifyRecommendLiveCacheEvicted(blockedMemberId)
|
||||
}
|
||||
|
||||
private fun verifyRecommendLiveCacheEvicted(memberId: Long) {
|
||||
Mockito.verify(cache).evict("getRecommendLive:$memberId:false")
|
||||
Mockito.verify(cache).evict("getRecommendLive:$memberId:true")
|
||||
Mockito.verify(cache).evict("getRecommendLive:$memberId")
|
||||
Mockito.verify(cache).evict("getRecommendLive:$blockedMemberId")
|
||||
}
|
||||
|
||||
private fun verifyRecommendLiveCacheNotEvicted(memberId: Long) {
|
||||
Mockito.verify(cache, Mockito.never()).evict("getRecommendLive:$memberId:false")
|
||||
Mockito.verify(cache, Mockito.never()).evict("getRecommendLive:$memberId:true")
|
||||
Mockito.verify(cache, Mockito.never()).evict("getRecommendLive:$memberId")
|
||||
}
|
||||
|
||||
private fun createMember(id: Long, nickname: String): Member {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package kr.co.vividnext.sodalive.member
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
|
||||
import kr.co.vividnext.sodalive.member.social.google.GoogleUserInfo
|
||||
import kr.co.vividnext.sodalive.member.stipulation.Stipulation
|
||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
|
||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds
|
||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
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.cache.CacheManager
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Optional
|
||||
|
||||
class MemberServiceContentPreferenceTest {
|
||||
private lateinit var repository: MemberRepository
|
||||
private lateinit var stipulationRepository: StipulationRepository
|
||||
private lateinit var stipulationAgreeRepository: StipulationAgreeRepository
|
||||
private lateinit var nicknameGenerateService: kr.co.vividnext.sodalive.member.nickname.NicknameGenerateService
|
||||
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
|
||||
private lateinit var chargeRepository: kr.co.vividnext.sodalive.can.charge.ChargeRepository
|
||||
private lateinit var memberPointRepository: kr.co.vividnext.sodalive.point.MemberPointRepository
|
||||
private lateinit var pushTokenService: kr.co.vividnext.sodalive.fcm.PushTokenService
|
||||
private lateinit var service: MemberService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = mock()
|
||||
stipulationRepository = mock()
|
||||
stipulationAgreeRepository = mock()
|
||||
nicknameGenerateService = mock()
|
||||
memberContentPreferenceService = mock()
|
||||
chargeRepository = mock()
|
||||
memberPointRepository = mock()
|
||||
pushTokenService = mock()
|
||||
|
||||
service = MemberService(
|
||||
repository = repository,
|
||||
tokenRepository = mock(),
|
||||
stipulationRepository = stipulationRepository,
|
||||
stipulationAgreeRepository = stipulationAgreeRepository,
|
||||
creatorFollowingRepository = mock(),
|
||||
blockMemberRepository = mock(),
|
||||
signOutRepository = mock(),
|
||||
nicknameChangeLogRepository = mock(),
|
||||
memberTagRepository = mock(),
|
||||
liveReservationRepository = mock(),
|
||||
chargeRepository = chargeRepository,
|
||||
memberPointRepository = memberPointRepository,
|
||||
orderService = mock(),
|
||||
emailService = mock(),
|
||||
pushTokenService = pushTokenService,
|
||||
canPaymentService = mock(),
|
||||
nicknameGenerateService = nicknameGenerateService,
|
||||
memberNotificationService = mock(),
|
||||
s3Uploader = mock(),
|
||||
validator = mock(),
|
||||
tokenProvider = mock(),
|
||||
passwordEncoder = mock(),
|
||||
authenticationManagerBuilder = mock(),
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
countryContext = CountryContext(),
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
objectMapper = ObjectMapper(),
|
||||
cacheManager = mock<CacheManager>(),
|
||||
s3Bucket = "test-bucket",
|
||||
cloudFrontHost = "https://cdn.test"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getMemberInfo는 저장된 콘텐츠 설정 필드를 그대로 반환한다")
|
||||
fun shouldReturnStoredPreferenceFieldsInMemberInfo() {
|
||||
val member = createMember(1L)
|
||||
member.createdAt = LocalDateTime.of(2026, 1, 1, 0, 0)
|
||||
val preference = ViewerContentPreference(
|
||||
countryCode = "JP",
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.MALE,
|
||||
isAdult = true
|
||||
)
|
||||
|
||||
Mockito.`when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn(preference)
|
||||
Mockito.`when`(chargeRepository.getChargeCount(1L)).thenReturn(3)
|
||||
Mockito.`when`(
|
||||
memberPointRepository.findByMemberIdAndExpiresAtAfterOrderByExpiresAtAsc(
|
||||
memberId = Mockito.eq(1L),
|
||||
expiresAt = anyLocalDateTime()
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
|
||||
val response = service.getMemberInfo(member, "web")
|
||||
|
||||
assertEquals("JP", response.countryCode)
|
||||
assertEquals(true, response.isAdultContentVisible)
|
||||
assertEquals(ContentType.MALE, response.contentType)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Google 소셜 회원 신규 생성 시 기본 콘텐츠 설정을 선저장한다")
|
||||
fun shouldInitializePreferenceWhenGoogleMemberIsRegistered() {
|
||||
var savedMember: Member? = null
|
||||
|
||||
val terms = Stipulation(title = "terms", description = "desc")
|
||||
terms.id = StipulationIds.TERMS_OF_SERVICE_ID
|
||||
val privacy = Stipulation(title = "privacy", description = "desc")
|
||||
privacy.id = StipulationIds.PRIVACY_POLICY_ID
|
||||
|
||||
Mockito.`when`(repository.findByGoogleId("sub-1")).thenReturn(null)
|
||||
Mockito.`when`(repository.findByEmail("google@test.com")).thenReturn(null)
|
||||
Mockito.`when`(stipulationRepository.findById(StipulationIds.TERMS_OF_SERVICE_ID)).thenReturn(Optional.of(terms))
|
||||
Mockito.`when`(stipulationRepository.findById(StipulationIds.PRIVACY_POLICY_ID)).thenReturn(Optional.of(privacy))
|
||||
Mockito.`when`(nicknameGenerateService.generateUniqueNickname(anyLang())).thenReturn("newbie")
|
||||
Mockito.`when`(repository.save(Mockito.any(Member::class.java))).thenAnswer { invocation ->
|
||||
val saved = invocation.getArgument<Member>(0)
|
||||
saved.id = 10L
|
||||
savedMember = saved
|
||||
saved
|
||||
}
|
||||
Mockito.`when`(stipulationAgreeRepository.save(Mockito.any())).thenAnswer { invocation -> invocation.getArgument(0) }
|
||||
|
||||
val result = service.findOrRegister(
|
||||
googleUserInfo = GoogleUserInfo(sub = "sub-1", email = "google@test.com", name = "google-user"),
|
||||
container = "web",
|
||||
marketingPid = null,
|
||||
pushToken = null
|
||||
)
|
||||
|
||||
assertTrue(result.isNew)
|
||||
Mockito.verify(memberContentPreferenceService).initializeDefaultPreference(savedMember!!)
|
||||
assertEquals(10L, savedMember!!.id)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long): Member {
|
||||
val member = Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id"
|
||||
)
|
||||
member.id = id
|
||||
return member
|
||||
}
|
||||
|
||||
private inline fun <reified T> mock(): T {
|
||||
return Mockito.mock(T::class.java)
|
||||
}
|
||||
|
||||
private fun anyLocalDateTime(): LocalDateTime =
|
||||
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
|
||||
|
||||
private fun anyLang(): Lang =
|
||||
Mockito.any(Lang::class.java) ?: Lang.KO
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package kr.co.vividnext.sodalive.member.auth
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.useraction.ActionType
|
||||
import kr.co.vividnext.sodalive.useraction.UserActionService
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class AuthControllerTest {
|
||||
private lateinit var authService: AuthService
|
||||
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
|
||||
private lateinit var userActionService: UserActionService
|
||||
private lateinit var controller: AuthController
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
authService = mock()
|
||||
memberContentPreferenceService = mock()
|
||||
userActionService = mock()
|
||||
|
||||
controller = AuthController(
|
||||
service = authService,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
userActionService = userActionService
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("authVerify 성공 시 성인노출 true 저장을 호출한다")
|
||||
fun shouldSaveAdultPreferenceWhenAuthVerifySucceeds() {
|
||||
val member = createMember(id = 10L)
|
||||
val request = AuthVerifyRequest(receiptId = "receipt-1", version = "v1")
|
||||
val certificate = AuthVerifyCertificate(
|
||||
name = "홍길동",
|
||||
birth = "19900101",
|
||||
unique = "unique-ci",
|
||||
di = "di-1",
|
||||
gender = 1
|
||||
)
|
||||
|
||||
Mockito.`when`(authService.certificate(request, memberId = 10L)).thenReturn(certificate)
|
||||
Mockito.`when`(authService.isBlockAuth(certificate)).thenReturn(false)
|
||||
Mockito.`when`(authService.authenticate(certificate, 10L)).thenReturn(AuthResponse(gender = 1))
|
||||
|
||||
controller.authVerify(request, member)
|
||||
|
||||
Mockito.verify(memberContentPreferenceService).markAdultVisibleAfterAuthVerify(10L)
|
||||
Mockito.verify(userActionService).recordAction(
|
||||
memberId = 10L,
|
||||
isAuth = true,
|
||||
actionType = ActionType.USER_AUTHENTICATION
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("차단 정책으로 authVerify가 실패하면 저장을 호출하지 않는다")
|
||||
fun shouldNotSaveAdultPreferenceWhenAuthIsBlocked() {
|
||||
val member = createMember(id = 20L)
|
||||
val request = AuthVerifyRequest(receiptId = "receipt-2", version = null)
|
||||
val certificate = AuthVerifyCertificate(
|
||||
name = "홍길동",
|
||||
birth = "19900101",
|
||||
unique = "unique-ci",
|
||||
di = "di-2",
|
||||
gender = 1
|
||||
)
|
||||
|
||||
Mockito.`when`(authService.certificate(request, memberId = 20L)).thenReturn(certificate)
|
||||
Mockito.`when`(authService.isBlockAuth(certificate)).thenReturn(true)
|
||||
|
||||
assertThrows(SodaException::class.java) {
|
||||
controller.authVerify(request, member)
|
||||
}
|
||||
|
||||
Mockito.verify(authService).signOut(20L)
|
||||
Mockito.verify(memberContentPreferenceService, Mockito.never()).markAdultVisibleAfterAuthVerify(Mockito.anyLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비로그인 사용자는 authVerify 요청 시 예외를 반환한다")
|
||||
fun shouldThrowWhenMemberIsNull() {
|
||||
val request = AuthVerifyRequest(receiptId = "receipt-3", version = null)
|
||||
|
||||
assertThrows(SodaException::class.java) {
|
||||
controller.authVerify(request, null)
|
||||
}
|
||||
|
||||
Mockito.verifyNoInteractions(memberContentPreferenceService)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long): Member {
|
||||
val member = Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id"
|
||||
)
|
||||
member.id = id
|
||||
return member
|
||||
}
|
||||
|
||||
private inline fun <reified T> mock(): T {
|
||||
return Mockito.mock(T::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package kr.co.vividnext.sodalive.member.contentpreference
|
||||
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.auth.AuthRepository
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
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.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
|
||||
import org.springframework.context.annotation.Import
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||
@Import(QueryDslConfig::class)
|
||||
class MemberContentPreferenceIntegrationTest @Autowired constructor(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val preferenceRepository: MemberContentPreferenceRepository,
|
||||
private val entityManager: EntityManager
|
||||
) {
|
||||
companion object {
|
||||
private val FORCED_MEMBER_IDS = setOf(2L, 16L, 17L, 29721L, 32050L, 40850L)
|
||||
}
|
||||
|
||||
private lateinit var service: MemberContentPreferenceService
|
||||
private lateinit var countryContext: CountryContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
countryContext = CountryContext()
|
||||
service = MemberContentPreferenceService(
|
||||
repository = preferenceRepository,
|
||||
memberRepository = memberRepository,
|
||||
countryContext = countryContext,
|
||||
cacheManager = ConcurrentMapCacheManager("cache_ttl_3_hours")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("legacy 파라미터 최초 호출 시 row를 생성하고 같은 흐름에서 저장값 조회에 즉시 반영한다")
|
||||
fun shouldCreateRowAndReflectImmediatelyOnFirstLegacyResolveCall() {
|
||||
val member = saveNonForcedMember("legacy-user")
|
||||
countryContext.setCountryCode("US")
|
||||
|
||||
assertEquals(null, preferenceRepository.findByMemberId(member.id!!))
|
||||
|
||||
val resolved = service.resolveForQuery(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.MALE
|
||||
)
|
||||
val stored = service.getStoredPreference(member)
|
||||
|
||||
assertNotNull(preferenceRepository.findByMemberId(member.id!!))
|
||||
assertTrue(resolved.isAdultContentVisible)
|
||||
assertEquals(ContentType.MALE, resolved.contentType)
|
||||
assertEquals("US", resolved.countryCode)
|
||||
assertTrue(stored.isAdultContentVisible)
|
||||
assertEquals(ContentType.MALE, stored.contentType)
|
||||
assertTrue(stored.isAdult)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("직접 설정 저장(updatePreference) 후 즉시 getStoredPreference에 반영된다")
|
||||
fun shouldPersistAndReflectAfterDirectUpdate() {
|
||||
val member = saveNonForcedMember("patch-user")
|
||||
countryContext.setCountryCode("US")
|
||||
|
||||
val updated = service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
val stored = service.getStoredPreference(member)
|
||||
|
||||
assertTrue(updated.isAdultContentVisible)
|
||||
assertEquals(ContentType.FEMALE, updated.contentType)
|
||||
assertTrue(stored.isAdultContentVisible)
|
||||
assertEquals(ContentType.FEMALE, stored.contentType)
|
||||
assertTrue(stored.isAdult)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("KR 헤더 누락 + 미인증 사용자는 요청값을 보내도 기본값을 유지한다")
|
||||
fun shouldKeepDefaultValuesForKrUnauthenticatedWhenHeaderMissing() {
|
||||
val member = saveNonForcedMember("kr-unauth-user")
|
||||
countryContext.setCountryCode(null)
|
||||
|
||||
val resolved = service.resolveForQuery(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.MALE
|
||||
)
|
||||
val stored = service.getStoredPreference(member)
|
||||
|
||||
assertEquals("KR", resolved.countryCode)
|
||||
assertFalse(resolved.isAdultContentVisible)
|
||||
assertEquals(ContentType.ALL, resolved.contentType)
|
||||
assertFalse(resolved.isAdult)
|
||||
assertFalse(stored.isAdultContentVisible)
|
||||
assertEquals(ContentType.ALL, stored.contentType)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("KR + 인증 사용자는 요청값이 저장되고 성인 조회값(isAdult)이 true로 계산된다")
|
||||
fun shouldApplyRequestValuesForKrAuthenticatedMember() {
|
||||
val member = saveNonForcedMember("kr-auth-user")
|
||||
countryContext.setCountryCode(null)
|
||||
saveAuth(member)
|
||||
|
||||
val reloadedMember = memberRepository.findById(member.id!!).orElseThrow()
|
||||
val resolved = service.resolveForQuery(
|
||||
member = reloadedMember,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
|
||||
assertEquals("KR", resolved.countryCode)
|
||||
assertTrue(resolved.isAdultContentVisible)
|
||||
assertEquals(ContentType.FEMALE, resolved.contentType)
|
||||
assertTrue(resolved.isAdult)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("authVerify 성공 후 markAdultVisibleAfterAuthVerify를 호출하면 저장값이 true로 반영된다")
|
||||
fun shouldMarkAdultVisibleAfterAuthVerify() {
|
||||
val member = saveNonForcedMember("auth-verified-user")
|
||||
countryContext.setCountryCode("US")
|
||||
|
||||
service.updatePreference(member, isAdultContentVisible = false, contentType = ContentType.ALL)
|
||||
service.markAdultVisibleAfterAuthVerify(member.id!!)
|
||||
|
||||
val stored = service.getStoredPreference(member)
|
||||
assertTrue(stored.isAdultContentVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("강제 매핑 회원 ID는 접속 국가 헤더보다 우선한다")
|
||||
fun shouldReturnForcedCountryCodeRegardlessOfHeader() {
|
||||
countryContext.setCountryCode("US")
|
||||
|
||||
val jpMember = Member(email = "jp@test.com", password = "password", nickname = "jp-member").apply { id = 2L }
|
||||
val krMember = Member(email = "kr@test.com", password = "password", nickname = "kr-member").apply { id = 16L }
|
||||
|
||||
assertEquals("JP", service.resolveCountryCode(jpMember))
|
||||
assertEquals("KR", service.resolveCountryCode(krMember))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("강제 매핑 대상이 아니면 국가 코드는 접속 국가 헤더를 기준으로 계산된다")
|
||||
fun shouldResolveCountryCodeByConnectionCountryHeaderForNonForcedMember() {
|
||||
val member = saveNonForcedMember("country-user")
|
||||
|
||||
countryContext.setCountryCode("US")
|
||||
assertEquals("US", service.resolveCountryCode(member))
|
||||
|
||||
countryContext.setCountryCode(null)
|
||||
assertEquals("KR", service.resolveCountryCode(member))
|
||||
}
|
||||
|
||||
private fun saveMember(seed: String): Member {
|
||||
return memberRepository.saveAndFlush(
|
||||
Member(
|
||||
email = "$seed@test.com",
|
||||
password = "password",
|
||||
nickname = seed
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveNonForcedMember(seed: String): Member {
|
||||
var index = 0
|
||||
while (true) {
|
||||
val candidate = saveMember("$seed-$index")
|
||||
if (!FORCED_MEMBER_IDS.contains(candidate.id)) {
|
||||
return candidate
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAuth(member: Member) {
|
||||
val auth = Auth(
|
||||
name = "홍길동",
|
||||
birth = "19900101",
|
||||
uniqueCi = "unique-ci-${member.id}",
|
||||
di = "di-${member.id}",
|
||||
gender = 1
|
||||
)
|
||||
auth.member = member
|
||||
authRepository.saveAndFlush(auth)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package kr.co.vividnext.sodalive.member.contentpreference
|
||||
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.mock.web.MockHttpServletRequest
|
||||
import org.springframework.web.context.request.RequestContextHolder
|
||||
import org.springframework.web.context.request.ServletRequestAttributes
|
||||
|
||||
class MemberContentPreferencePolicyTest {
|
||||
@AfterEach
|
||||
fun cleanup() {
|
||||
RequestContextHolder.resetRequestAttributes()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("요청 국가 헤더를 기준으로 국가 코드를 계산한다")
|
||||
fun shouldResolveCountryCodeByRequestHeader() {
|
||||
setRequestCountry(" us ")
|
||||
val member = createMember(id = 200L, countryCode = "KR")
|
||||
|
||||
assertEquals("US", resolveCountryCodeByPolicy(member))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("강제 매핑 대상 회원 ID는 요청 국가 헤더보다 우선한다")
|
||||
fun shouldPrioritizeForcedCountryMapping() {
|
||||
setRequestCountry("US")
|
||||
|
||||
val forcedJpMember = createMember(id = 2L, countryCode = "KR")
|
||||
val forcedKrMember = createMember(id = 16L, countryCode = "US")
|
||||
|
||||
assertEquals("JP", resolveCountryCodeByPolicy(forcedJpMember))
|
||||
assertEquals("KR", resolveCountryCodeByPolicy(forcedKrMember))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("요청 국가가 KR이면 인증 미완료 사용자는 성인 노출이 false다")
|
||||
fun shouldHideAdultContentForKrWithoutAuth() {
|
||||
setRequestCountry("KR")
|
||||
val member = createMember(id = 1L, countryCode = "US")
|
||||
|
||||
assertFalse(isAdultVisibleByPolicy(member, isAdultContentVisible = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("요청 국가가 KR이 아니면 멤버 countryCode와 무관하게 전달값을 사용한다")
|
||||
fun shouldIgnoreStoredCountryCodeWhenRequestCountryIsNotKr() {
|
||||
setRequestCountry("US")
|
||||
val member = createMember(id = 201L, countryCode = "KR")
|
||||
|
||||
assertTrue(isAdultVisibleByPolicy(member, isAdultContentVisible = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("요청 컨텍스트가 없으면 KR fallback 정책을 사용한다")
|
||||
fun shouldFallbackToKrWhenRequestContextIsMissing() {
|
||||
RequestContextHolder.resetRequestAttributes()
|
||||
val member = createMember(id = 202L, countryCode = "US")
|
||||
|
||||
assertEquals("KR", resolveCountryCodeByPolicy(member))
|
||||
assertFalse(isAdultVisibleByPolicy(member, isAdultContentVisible = true))
|
||||
}
|
||||
|
||||
private fun setRequestCountry(countryCode: String?) {
|
||||
val request = MockHttpServletRequest()
|
||||
if (countryCode != null) {
|
||||
request.addHeader("CloudFront-Viewer-Country", countryCode)
|
||||
}
|
||||
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(request))
|
||||
}
|
||||
|
||||
private fun createMember(id: Long, countryCode: String?): Member {
|
||||
return Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id"
|
||||
).apply {
|
||||
this.id = id
|
||||
this.countryCode = countryCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package kr.co.vividnext.sodalive.member.contentpreference
|
||||
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
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.cache.Cache
|
||||
import org.springframework.cache.CacheManager
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Optional
|
||||
|
||||
class MemberContentPreferenceServiceTest {
|
||||
private lateinit var repository: MemberContentPreferenceRepository
|
||||
private lateinit var memberRepository: MemberRepository
|
||||
private lateinit var countryContext: CountryContext
|
||||
private lateinit var cacheManager: CacheManager
|
||||
private lateinit var recommendLiveCache: Cache
|
||||
private lateinit var service: MemberContentPreferenceService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = mock()
|
||||
memberRepository = mock()
|
||||
countryContext = CountryContext()
|
||||
cacheManager = mock()
|
||||
recommendLiveCache = mock()
|
||||
Mockito.`when`(cacheManager.getCache("cache_ttl_3_hours")).thenReturn(recommendLiveCache)
|
||||
|
||||
service = MemberContentPreferenceService(
|
||||
repository = repository,
|
||||
memberRepository = memberRepository,
|
||||
countryContext = countryContext,
|
||||
cacheManager = cacheManager
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("회원 ID 강제 매핑(KR)이 헤더보다 우선 적용된다")
|
||||
fun shouldResolveCountryCodeByForcedKrMappingFirst() {
|
||||
val member = createMember(id = 16L)
|
||||
val preference = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(16L)).thenReturn(preference)
|
||||
|
||||
val result = service.getStoredPreference(member)
|
||||
|
||||
assertEquals("KR", result.countryCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("회원 ID 강제 매핑(JP)이 헤더보다 우선 적용된다")
|
||||
fun shouldResolveCountryCodeByForcedJapanMappingFirst() {
|
||||
val member = createMember(id = 2L)
|
||||
val preference = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(2L)).thenReturn(preference)
|
||||
|
||||
val result = service.getStoredPreference(member)
|
||||
|
||||
assertEquals("JP", result.countryCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("강제 매핑 대상이 아니면 접속 국가 헤더를 사용하고 헤더가 없으면 KR로 fallback 한다")
|
||||
fun shouldResolveCountryCodeWithHeaderAndFallback() {
|
||||
val member = createMember(id = 100L)
|
||||
val preference = createPreference(member)
|
||||
Mockito.`when`(repository.findByMemberId(100L)).thenReturn(preference)
|
||||
|
||||
countryContext.setCountryCode("JP")
|
||||
val fromHeader = service.getStoredPreference(member)
|
||||
assertEquals("JP", fromHeader.countryCode)
|
||||
|
||||
countryContext.setCountryCode(null)
|
||||
val fromFallback = service.getStoredPreference(member)
|
||||
assertEquals("KR", fromFallback.countryCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("한국 + 본인인증 미완료는 전달값으로 저장 갱신하지 않는다")
|
||||
fun shouldNotApplyRequestValuesForKoreaWithoutAuth() {
|
||||
val member = createMember(id = 1700L)
|
||||
val baselineTime = LocalDateTime.of(2025, 1, 1, 0, 0)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.ALL,
|
||||
adultContentVisibilityChangedAt = baselineTime,
|
||||
contentTypeChangedAt = baselineTime
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("KR")
|
||||
Mockito.`when`(repository.findByMemberId(1700L)).thenReturn(preference)
|
||||
|
||||
val result = service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
|
||||
assertFalse(result.isAdultContentVisible)
|
||||
assertEquals(ContentType.ALL, result.contentType)
|
||||
assertEquals(baselineTime, preference.adultContentVisibilityChangedAt)
|
||||
assertEquals(baselineTime, preference.contentTypeChangedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("해외 + 본인인증 미완료는 전달값을 그대로 저장한다")
|
||||
fun shouldApplyRequestValuesForNonKoreaWithoutAuth() {
|
||||
val member = createMember(id = 1000L)
|
||||
val preference = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(1000L)).thenReturn(preference)
|
||||
|
||||
val result = service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
|
||||
assertEquals("US", result.countryCode)
|
||||
assertTrue(result.isAdultContentVisible)
|
||||
assertEquals(ContentType.FEMALE, result.contentType)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("한국 + 본인인증 완료는 전달값을 저장한다")
|
||||
fun shouldApplyRequestValuesForKoreaWithAuth() {
|
||||
val member = createMember(id = 1701L, withAuth = true)
|
||||
val baselineTime = LocalDateTime.of(2025, 1, 1, 0, 0)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.ALL,
|
||||
adultContentVisibilityChangedAt = baselineTime,
|
||||
contentTypeChangedAt = baselineTime
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("KR")
|
||||
Mockito.`when`(repository.findByMemberId(1701L)).thenReturn(preference)
|
||||
|
||||
val result = service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.FEMALE
|
||||
)
|
||||
|
||||
assertEquals("KR", result.countryCode)
|
||||
assertTrue(result.isAdultContentVisible)
|
||||
assertEquals(ContentType.FEMALE, result.contentType)
|
||||
assertTrue(preference.adultContentVisibilityChangedAt.isAfter(baselineTime))
|
||||
assertTrue(preference.contentTypeChangedAt.isAfter(baselineTime))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("필드별 변경 시 changedAt은 변경된 필드만 갱신된다")
|
||||
fun shouldUpdateOnlyChangedFieldTimestamp() {
|
||||
val member = createMember(id = 3000L, withAuth = true)
|
||||
val baselineTime = LocalDateTime.of(2025, 1, 1, 0, 0)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.ALL,
|
||||
adultContentVisibilityChangedAt = baselineTime,
|
||||
contentTypeChangedAt = baselineTime
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("KR")
|
||||
Mockito.`when`(repository.findByMemberId(3000L)).thenReturn(preference)
|
||||
|
||||
service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
|
||||
assertTrue(preference.adultContentVisibilityChangedAt.isAfter(baselineTime))
|
||||
assertEquals(baselineTime, preference.contentTypeChangedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("contentType만 변경하면 contentTypeChangedAt만 갱신된다")
|
||||
fun shouldUpdateOnlyContentTypeChangedAtWhenContentTypeChanges() {
|
||||
val member = createMember(id = 18L, withAuth = true)
|
||||
val baselineTime = LocalDateTime.of(2025, 1, 1, 0, 0)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.ALL,
|
||||
adultContentVisibilityChangedAt = baselineTime,
|
||||
contentTypeChangedAt = baselineTime
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("KR")
|
||||
Mockito.`when`(repository.findByMemberId(18L)).thenReturn(preference)
|
||||
|
||||
service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.MALE
|
||||
)
|
||||
|
||||
assertEquals(baselineTime, preference.adultContentVisibilityChangedAt)
|
||||
assertTrue(preference.contentTypeChangedAt.isAfter(baselineTime))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일값 재저장 시 changedAt은 갱신되지 않는다")
|
||||
fun shouldNotUpdateChangedAtWhenValuesAreSame() {
|
||||
val member = createMember(id = 19L, withAuth = true)
|
||||
val baselineTime = LocalDateTime.of(2025, 1, 1, 0, 0)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.MALE,
|
||||
adultContentVisibilityChangedAt = baselineTime,
|
||||
contentTypeChangedAt = baselineTime
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("KR")
|
||||
Mockito.`when`(repository.findByMemberId(19L)).thenReturn(preference)
|
||||
|
||||
service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.MALE
|
||||
)
|
||||
|
||||
assertEquals(baselineTime, preference.adultContentVisibilityChangedAt)
|
||||
assertEquals(baselineTime, preference.contentTypeChangedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getStoredPreference 호출 시 row가 없으면 기본값 row를 생성한다")
|
||||
fun shouldCreateDefaultPreferenceWhenRowIsMissing() {
|
||||
val member = createMember(id = 20L)
|
||||
countryContext.setCountryCode(null)
|
||||
val storedPreference = createPreference(member)
|
||||
|
||||
Mockito.`when`(repository.findByMemberId(20L)).thenReturn(null)
|
||||
Mockito.`when`(memberRepository.findByIdForUpdate(20L)).thenReturn(member)
|
||||
Mockito.`when`(repository.findByMemberIdForUpdate(20L)).thenReturn(null)
|
||||
Mockito.`when`(repository.saveAndFlush(Mockito.any(MemberContentPreference::class.java)))
|
||||
.thenReturn(storedPreference)
|
||||
|
||||
val result = service.getStoredPreference(member)
|
||||
|
||||
assertEquals("KR", result.countryCode)
|
||||
assertEquals(storedPreference.isAdultContentVisible, result.isAdultContentVisible)
|
||||
assertEquals(ContentType.ALL, result.contentType)
|
||||
Mockito.verify(repository).saveAndFlush(Mockito.any(MemberContentPreference::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("초기 row 생성 경쟁 시 잠금 이후 재조회한 row를 반환한다")
|
||||
fun shouldReturnReloadedPreferenceWhenRowIsCreatedByAnotherTransactionAfterLock() {
|
||||
val member = createMember(id = 26L)
|
||||
val existing = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
|
||||
Mockito.`when`(repository.findByMemberId(26L)).thenReturn(null)
|
||||
Mockito.`when`(memberRepository.findByIdForUpdate(26L)).thenReturn(member)
|
||||
Mockito.`when`(repository.findByMemberIdForUpdate(26L)).thenReturn(existing)
|
||||
|
||||
val result = service.getStoredPreference(member)
|
||||
|
||||
assertEquals(existing.isAdultContentVisible, result.isAdultContentVisible)
|
||||
assertEquals(existing.contentType, result.contentType)
|
||||
Mockito.verify(repository, Mockito.never()).saveAndFlush(Mockito.any(MemberContentPreference::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동시 insert 충돌 발생 시 저장된 row를 재조회해 반환한다")
|
||||
fun shouldReturnStoredRowWhenDuplicateInsertOccurs() {
|
||||
val member = createMember(id = 27L)
|
||||
val stored = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
|
||||
Mockito.`when`(repository.findByMemberId(27L)).thenReturn(null)
|
||||
Mockito.`when`(memberRepository.findByIdForUpdate(27L)).thenReturn(member)
|
||||
Mockito.`when`(repository.findByMemberIdForUpdate(27L)).thenReturn(null, stored)
|
||||
Mockito.`when`(repository.saveAndFlush(Mockito.any(MemberContentPreference::class.java)))
|
||||
.thenThrow(DataIntegrityViolationException("duplicate"))
|
||||
|
||||
val result = service.getStoredPreference(member)
|
||||
|
||||
assertEquals(stored.isAdultContentVisible, result.isAdultContentVisible)
|
||||
assertEquals(stored.contentType, result.contentType)
|
||||
Mockito.verify(repository).saveAndFlush(Mockito.any(MemberContentPreference::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("직접 설정으로 저장값이 변경되면 추천 라이브 캐시를 무효화한다")
|
||||
fun shouldEvictRecommendLiveCacheWhenPreferenceChangesByUpdatePreference() {
|
||||
val member = createMember(id = 30L, withAuth = true)
|
||||
val preference = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(30L)).thenReturn(preference)
|
||||
|
||||
service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
|
||||
verifyRecommendLiveCacheEvicted(30L)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("직접 설정 값이 동일하면 추천 라이브 캐시를 무효화하지 않는다")
|
||||
fun shouldNotEvictRecommendLiveCacheWhenPreferenceIsUnchanged() {
|
||||
val member = createMember(id = 31L, withAuth = true)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.ALL,
|
||||
adultContentVisibilityChangedAt = LocalDateTime.now().minusDays(1),
|
||||
contentTypeChangedAt = LocalDateTime.now().minusDays(1)
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(31L)).thenReturn(preference)
|
||||
|
||||
service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
|
||||
verifyRecommendLiveCacheNotEvicted(31L)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("authVerify 연동으로 성인 노출이 true로 바뀌면 추천 라이브 캐시를 무효화한다")
|
||||
fun shouldEvictRecommendLiveCacheWhenMarkAdultVisibleAfterAuthVerifyChangesValue() {
|
||||
val member = createMember(id = 32L)
|
||||
val preference = createPreference(member)
|
||||
Mockito.`when`(memberRepository.findById(32L)).thenReturn(Optional.of(member))
|
||||
Mockito.`when`(repository.findByMemberId(32L)).thenReturn(preference)
|
||||
|
||||
service.markAdultVisibleAfterAuthVerify(32L)
|
||||
|
||||
verifyRecommendLiveCacheEvicted(32L)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("contentType 미전달 조회는 기존 contentType을 유지한다")
|
||||
fun shouldKeepStoredContentTypeWhenContentTypeIsNotProvided() {
|
||||
val member = createMember(id = 21L, withAuth = true)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.FEMALE,
|
||||
adultContentVisibilityChangedAt = LocalDateTime.now().minusDays(2),
|
||||
contentTypeChangedAt = LocalDateTime.now().minusDays(2)
|
||||
)
|
||||
preference.member = member
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(21L)).thenReturn(preference)
|
||||
|
||||
val result = service.resolveForQuery(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = null
|
||||
)
|
||||
|
||||
assertEquals(ContentType.FEMALE, result.contentType)
|
||||
assertTrue(result.isAdultContentVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("legacy 조회 파라미터로 저장값이 바뀌면 추천 라이브 캐시를 무효화한다")
|
||||
fun shouldEvictRecommendLiveCacheWhenPreferenceChangesByLegacyResolveForQuery() {
|
||||
val member = createMember(id = 25L, withAuth = true)
|
||||
val preference = createPreference(member)
|
||||
countryContext.setCountryCode("US")
|
||||
Mockito.`when`(repository.findByMemberId(25L)).thenReturn(preference)
|
||||
|
||||
service.resolveForQuery(
|
||||
member = member,
|
||||
isAdultContentVisible = true,
|
||||
contentType = null
|
||||
)
|
||||
|
||||
verifyRecommendLiveCacheEvicted(25L)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("한국/해외 조회 정책은 인증 여부와 국가코드에 따라 다르게 계산된다")
|
||||
fun shouldCalculateIsAdultByCountryPolicy() {
|
||||
val noAuthMember = createMember(id = 22L, withAuth = false)
|
||||
val authMember = createMember(id = 23L, withAuth = true)
|
||||
|
||||
assertFalse(service.calculateIsAdultForQuery(noAuthMember, "KR", true))
|
||||
assertTrue(service.calculateIsAdultForQuery(authMember, "KR", true))
|
||||
assertTrue(service.calculateIsAdultForQuery(noAuthMember, "US", true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("직접 설정 API 입력이 모두 누락되면 예외를 발생시킨다")
|
||||
fun shouldThrowWhenAllPreferenceFieldsAreMissing() {
|
||||
val member = createMember(id = 24L, withAuth = true)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.updatePreference(
|
||||
member = member,
|
||||
isAdultContentVisible = null,
|
||||
contentType = null
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||
}
|
||||
|
||||
private fun createPreference(member: Member): MemberContentPreference {
|
||||
val now = LocalDateTime.now().minusDays(1)
|
||||
val preference = MemberContentPreference(
|
||||
isAdultContentVisible = false,
|
||||
contentType = ContentType.ALL,
|
||||
adultContentVisibilityChangedAt = now,
|
||||
contentTypeChangedAt = now
|
||||
)
|
||||
preference.member = member
|
||||
return preference
|
||||
}
|
||||
|
||||
private fun createMember(id: Long, withAuth: Boolean = false): Member {
|
||||
val member = Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id"
|
||||
)
|
||||
member.id = id
|
||||
|
||||
if (withAuth) {
|
||||
val auth = Auth(
|
||||
name = "홍길동",
|
||||
birth = "19900101",
|
||||
uniqueCi = "unique-$id",
|
||||
di = "di-$id",
|
||||
gender = 1
|
||||
)
|
||||
auth.member = member
|
||||
}
|
||||
|
||||
return member
|
||||
}
|
||||
|
||||
private fun verifyRecommendLiveCacheEvicted(memberId: Long) {
|
||||
Mockito.verify(recommendLiveCache).evict("getRecommendLive:$memberId:false")
|
||||
Mockito.verify(recommendLiveCache).evict("getRecommendLive:$memberId:true")
|
||||
Mockito.verify(recommendLiveCache).evict("getRecommendLive:$memberId")
|
||||
}
|
||||
|
||||
private fun verifyRecommendLiveCacheNotEvicted(memberId: Long) {
|
||||
Mockito.verify(recommendLiveCache, Mockito.never()).evict("getRecommendLive:$memberId:false")
|
||||
Mockito.verify(recommendLiveCache, Mockito.never()).evict("getRecommendLive:$memberId:true")
|
||||
Mockito.verify(recommendLiveCache, Mockito.never()).evict("getRecommendLive:$memberId")
|
||||
}
|
||||
|
||||
private inline fun <reified T> mock(): T {
|
||||
return Mockito.mock(T::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package kr.co.vividnext.sodalive.search
|
||||
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class SearchServiceTest {
|
||||
private val repository: SearchRepository = Mockito.mock(SearchRepository::class.java)
|
||||
private val service = SearchService(repository)
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 검색은 전달받은 isAdult 값을 그대로 사용한다")
|
||||
fun shouldUseProvidedIsAdultForContentSearch() {
|
||||
val member = createMember(id = 101L, countryCode = "KR")
|
||||
val contentItem = SearchResponseItem(
|
||||
id = 10L,
|
||||
imageUrl = "https://cdn.test/content.png",
|
||||
title = "title",
|
||||
nickname = "creator"
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
repository.searchContentTotalCount(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
).thenReturn(1)
|
||||
Mockito.`when`(
|
||||
repository.searchContentList(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 10
|
||||
)
|
||||
).thenReturn(listOf(contentItem))
|
||||
|
||||
val result = service.searchContentList(
|
||||
keyword = "keyword",
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
member = member,
|
||||
offset = 0,
|
||||
limit = 10
|
||||
)
|
||||
|
||||
assertEquals(1, result.totalCount)
|
||||
assertEquals(SearchResponseType.CONTENT, result.items.first().type)
|
||||
Mockito.verify(repository).searchContentTotalCount(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
Mockito.verify(repository, Mockito.never()).searchContentTotalCount(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = false,
|
||||
contentType = ContentType.ALL
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("통합 검색은 전달받은 isAdult 값으로 콘텐츠/시리즈 조회를 수행한다")
|
||||
fun shouldUseProvidedIsAdultForUnifiedSearch() {
|
||||
val member = createMember(id = 102L, countryCode = "KR")
|
||||
val creatorItem = SearchResponseItem(
|
||||
id = 20L,
|
||||
imageUrl = "https://cdn.test/creator.png",
|
||||
title = "creator",
|
||||
nickname = "creator"
|
||||
)
|
||||
val contentItem = SearchResponseItem(
|
||||
id = 21L,
|
||||
imageUrl = "https://cdn.test/content.png",
|
||||
title = "content",
|
||||
nickname = "creator"
|
||||
)
|
||||
val seriesItem = SearchResponseItem(
|
||||
id = 22L,
|
||||
imageUrl = "https://cdn.test/series.png",
|
||||
title = "series",
|
||||
nickname = "creator"
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
repository.searchCreatorList(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
offset = 0,
|
||||
limit = 3
|
||||
)
|
||||
).thenReturn(listOf(creatorItem))
|
||||
Mockito.`when`(
|
||||
repository.searchContentList(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 3
|
||||
)
|
||||
).thenReturn(listOf(contentItem))
|
||||
Mockito.`when`(
|
||||
repository.searchSeriesList(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 3
|
||||
)
|
||||
).thenReturn(listOf(seriesItem))
|
||||
|
||||
val result = service.searchUnified(
|
||||
keyword = "keyword",
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
member = member
|
||||
)
|
||||
|
||||
assertEquals(SearchResponseType.CREATOR, result.creatorList.first().type)
|
||||
assertEquals(SearchResponseType.CONTENT, result.contentList.first().type)
|
||||
assertEquals(SearchResponseType.SERIES, result.seriesList.first().type)
|
||||
Mockito.verify(repository).searchContentList(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 3
|
||||
)
|
||||
Mockito.verify(repository).searchSeriesList(
|
||||
keyword = "keyword",
|
||||
memberId = member.id!!,
|
||||
isAdult = true,
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 3
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long, countryCode: String?): Member {
|
||||
return Member(
|
||||
email = "member$id@test.com",
|
||||
password = "password",
|
||||
nickname = "member$id"
|
||||
).apply {
|
||||
this.id = id
|
||||
this.countryCode = countryCode
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user