package kr.co.vividnext.sodalive.member import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.common.CountryContext import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.auth.Auth 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 import org.mockito.Mockito import org.springframework.cache.Cache import org.springframework.cache.CacheManager import java.util.Optional class MemberServiceCacheEvictionTest { private lateinit var memberRepository: MemberRepository private lateinit var blockMemberRepository: BlockMemberRepository private lateinit var authRepository: AuthRepository private lateinit var cacheManager: CacheManager private lateinit var cache: Cache private lateinit var service: MemberService @BeforeEach fun setup() { memberRepository = mock() blockMemberRepository = mock() authRepository = mock() cacheManager = mock() cache = mock() Mockito.`when`(cacheManager.getCache("cache_ttl_3_hours")).thenReturn(cache) service = MemberService( repository = memberRepository, tokenRepository = mock(), stipulationRepository = mock(), stipulationAgreeRepository = mock(), creatorFollowingRepository = mock(), blockMemberRepository = blockMemberRepository, signOutRepository = mock(), nicknameChangeLogRepository = mock(), memberTagRepository = mock(), liveReservationRepository = mock(), chargeRepository = mock(), memberPointRepository = mock(), orderService = mock(), emailService = mock(), pushTokenService = mock(), canPaymentService = mock(), nicknameGenerateService = mock(), memberNotificationService = mock(), s3Uploader = mock(), validator = mock(), tokenProvider = mock(), passwordEncoder = mock(), authenticationManagerBuilder = mock(), messageSource = SodaMessageSource(), langContext = LangContext(), countryContext = CountryContext(), memberContentPreferenceService = mock(), objectMapper = ObjectMapper(), cacheManager = cacheManager, s3Bucket = "test-bucket", cloudFrontHost = "https://cdn.test" ) } @Test fun shouldEvictRecommendLiveCacheForRequesterAndTargetOnBlock() { val memberId = 100L val blockedMemberId = 200L val member = createMember(id = memberId, nickname = "requester") val blockedMember = createMember(id = blockedMemberId, nickname = "target") Mockito.`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(member)) Mockito.`when`(memberRepository.findById(blockedMemberId)).thenReturn(Optional.of(blockedMember)) Mockito.`when`( blockMemberRepository.getBlockAccount( blockedMemberId = blockedMemberId, memberId = memberId ) ).thenReturn(null) service.memberBlock(MemberBlockRequest(blockMemberId = blockedMemberId), memberId) verifyRecommendLiveCacheEvicted(memberId) verifyRecommendLiveCacheEvicted(blockedMemberId) Mockito.verifyNoInteractions(authRepository) } @Test fun shouldBlockOnlyRequestedMemberEvenWhenTargetHasAuth() { // 차단 대상에게 본인인증 정보가 연결된 상황을 준비한다. val memberId = 500L val blockedMemberId = 600L val linkedMemberId = 601L val member = createMember(id = memberId, nickname = "requester2") val blockedMember = createMember(id = blockedMemberId, nickname = "target2") val auth = Auth( name = "홍길동", birth = "19900101", uniqueCi = "unique-ci", di = "di-value", gender = 1 ) auth.member = blockedMember // 요청자와 요청 대상만 조회 가능하도록 목 동작을 설정한다. Mockito.`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(member)) Mockito.`when`(memberRepository.findById(blockedMemberId)).thenReturn(Optional.of(blockedMember)) Mockito.`when`( blockMemberRepository.getBlockAccount( blockedMemberId = blockedMemberId, memberId = memberId ) ).thenReturn(null) Mockito.`when`( authRepository.getMemberIdsByNameAndBirthAndDiAndGender( name = auth.name, birth = auth.birth, di = auth.di, gender = auth.gender ) ).thenReturn(listOf(blockedMemberId, linkedMemberId)) // 차단 API를 실행한다. service.memberBlock(MemberBlockRequest(blockMemberId = blockedMemberId), memberId) // 요청한 blockMemberId 한 건만 차단 처리 및 캐시 무효화되는지 검증한다. Mockito.verify(blockMemberRepository).getBlockAccount( blockedMemberId = blockedMemberId, memberId = memberId ) Mockito.verify(blockMemberRepository, Mockito.never()).getBlockAccount( blockedMemberId = linkedMemberId, memberId = memberId ) verifyRecommendLiveCacheEvicted(memberId) verifyRecommendLiveCacheEvicted(blockedMemberId) verifyRecommendLiveCacheNotEvicted(linkedMemberId) Mockito.verifyNoInteractions(authRepository) } @Test fun shouldEvictRecommendLiveCacheForRequesterAndTargetOnUnblock() { val memberId = 300L val blockedMemberId = 400L val blockMember = BlockMember(isActive = true) Mockito.`when`( blockMemberRepository.getBlockAccount( blockedMemberId = blockedMemberId, memberId = memberId ) ).thenReturn(blockMember) 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") } 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 { val member = Member( email = "$nickname@test.com", password = "password", nickname = nickname ) member.id = id return member } private inline fun mock(): T { return Mockito.mock(T::class.java) } }