fix(recommend-live): 차단 관계를 추천 조회에 반영하고 캐시를 무효화한다
This commit is contained in:
17
docs/20260226_라이브추천차단조인및캐시무효화.md
Normal file
17
docs/20260226_라이브추천차단조인및캐시무효화.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 라이브 추천 차단 JOIN 및 캐시 무효화
|
||||||
|
|
||||||
|
- [x] `LiveRecommendService.getRecommendLive`의 차단 필터 처리 구조 점검
|
||||||
|
- [x] `LiveRecommendRepository.getRecommendLive`를 DB 조회 시 차단 관계를 JOIN/조건으로 제외하도록 변경
|
||||||
|
- [x] 차단(`memberBlock`) 및 차단 해제(`memberUnBlock`) 시 추천 라이브 캐시가 즉시 반영되도록 무효화 처리
|
||||||
|
- [x] 변경 코드 정적 진단 및 테스트/빌드 검증
|
||||||
|
- [x] 검증 기록 작성
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 1차 구현
|
||||||
|
- 무엇을: `getRecommendLive`의 차단 제외 로직을 서비스 단 필터링에서 QueryDSL `leftJoin(blockMember)` + `blockMember.id.isNull` 조건으로 이동했고, 차단/차단해제 시 `CacheManager`로 `getRecommendLive:{memberId}` 키를 직접 evict 하도록 적용했다.
|
||||||
|
- 왜: 기존 방식은 추천 결과 조회 후 creator마다 `isBlocked`를 반복 호출해 후처리하고, 캐시 만료 전까지 차단/해제 결과가 반영되지 않는 문제가 있어 DB 레벨 필터링과 이벤트성 캐시 무효화가 필요했다.
|
||||||
|
- 어떻게:
|
||||||
|
- `lsp_diagnostics` (대상: `LiveRecommendRepository.kt`, `LiveRecommendService.kt`, `MemberService.kt`) 실행 결과: **환경상 Kotlin LSP 미구성으로 진단 불가**
|
||||||
|
- `./gradlew test` 실행 결과: **성공 (BUILD SUCCESSFUL)**
|
||||||
|
- `./gradlew build` 실행 결과: **성공 (BUILD SUCCESSFUL, ktlint/check 포함)**
|
||||||
21
docs/20260226_라이브추천차단조인캐시무효화검증테스트.md
Normal file
21
docs/20260226_라이브추천차단조인캐시무효화검증테스트.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 라이브 추천 차단 JOIN/캐시 무효화 검증 테스트
|
||||||
|
|
||||||
|
- [x] `LiveRecommendRepository.getRecommendLive`가 차단 관계(`member -> creator`, `creator -> member`)를 DB 조회 단계에서 제외하는지 테스트 추가
|
||||||
|
- [x] `LiveRecommendService.getRecommendLive`가 서비스 단 후처리 없이 저장소 결과를 그대로 위임하는지 테스트 추가
|
||||||
|
- [x] `MemberService.memberBlock`/`memberUnBlock` 호출 시 추천 라이브 캐시 키(`getRecommendLive:{memberId}`)가 즉시 무효화되는지 테스트 추가
|
||||||
|
- [x] 테스트 및 빌드 검증 수행
|
||||||
|
- [x] 검증 기록 작성
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 1차 검증 테스트 구현
|
||||||
|
- 무엇을: 문서 요구사항(추천 라이브 차단 JOIN, 서비스 위임 구조, 차단/해제 시 캐시 무효화)을 검증하는 테스트 3종을 추가했다.
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepositoryTest.kt`
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendServiceTest.kt`
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceCacheEvictionTest.kt`
|
||||||
|
- 왜: `docs/20260226_라이브추천차단조인및캐시무효화.md`에 기재된 구현이 실제 코드에서 회귀 없이 유지되는지 자동 검증이 필요하다.
|
||||||
|
- 어떻게:
|
||||||
|
- `lsp_diagnostics` (대상: 위 3개 Kotlin 테스트 파일) 실행 결과: **환경상 Kotlin LSP 미구성으로 진단 불가**
|
||||||
|
- `./gradlew test --tests "kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepositoryTest" --tests "kr.co.vividnext.sodalive.live.recommend.LiveRecommendServiceTest" --tests "kr.co.vividnext.sodalive.member.MemberServiceCacheEvictionTest"` 실행 결과: **성공 (BUILD SUCCESSFUL)**
|
||||||
|
- `./gradlew build` 1차 실행 결과: **실패 (`MemberServiceCacheEvictionTest.kt` 라인 길이/인자 줄바꿈 ktlint 위반)**
|
||||||
|
- `MemberServiceCacheEvictionTest.kt` 포맷 수정 후 `./gradlew build` 재실행 결과: **성공 (BUILD SUCCESSFUL, test/check/ktlint 통과)**
|
||||||
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recom
|
|||||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
import kr.co.vividnext.sodalive.member.QMember.member
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
||||||
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
@@ -20,7 +21,7 @@ class LiveRecommendRepository(
|
|||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) {
|
||||||
fun getRecommendLive(
|
fun getRecommendLive(
|
||||||
isBlocked: (Long) -> Boolean,
|
memberId: Long?,
|
||||||
isAdult: Boolean
|
isAdult: Boolean
|
||||||
): List<GetRecommendLiveResponse> {
|
): List<GetRecommendLiveResponse> {
|
||||||
val dateNow = LocalDateTime.now()
|
val dateNow = LocalDateTime.now()
|
||||||
@@ -32,7 +33,7 @@ class LiveRecommendRepository(
|
|||||||
where = where.and(recommendLiveCreatorBanner.isAdult.isFalse)
|
where = where.and(recommendLiveCreatorBanner.isAdult.isFalse)
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryFactory
|
var select = queryFactory
|
||||||
.select(
|
.select(
|
||||||
Projections.constructor(
|
Projections.constructor(
|
||||||
GetRecommendLiveResponse::class.java,
|
GetRecommendLiveResponse::class.java,
|
||||||
@@ -41,12 +42,26 @@ class LiveRecommendRepository(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.from(recommendLiveCreatorBanner)
|
.from(recommendLiveCreatorBanner)
|
||||||
|
|
||||||
|
if (memberId != null) {
|
||||||
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(recommendLiveCreatorBanner.creator.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(recommendLiveCreatorBanner.creator.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
where = where.and(blockMember.id.isNull)
|
||||||
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return select
|
||||||
.where(where)
|
.where(where)
|
||||||
.orderBy(recommendLiveCreatorBanner.orders.asc())
|
.orderBy(recommendLiveCreatorBanner.orders.asc())
|
||||||
.fetch()
|
.fetch()
|
||||||
.asSequence()
|
|
||||||
.filter { !isBlocked(it.creatorId) }
|
|
||||||
.toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOnAirRecommendChannelList(
|
fun getOnAirRecommendChannelList(
|
||||||
|
|||||||
@@ -16,17 +16,11 @@ class LiveRecommendService(
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = ["cache_ttl_3_hours"],
|
cacheNames = ["cache_ttl_3_hours"],
|
||||||
key = "'getRecommendLive:' + (#member ?: 'guest')"
|
key = "'getRecommendLive:' + (#member?.id ?: 'guest')"
|
||||||
)
|
)
|
||||||
fun getRecommendLive(member: Member?): List<GetRecommendLiveResponse> {
|
fun getRecommendLive(member: Member?): List<GetRecommendLiveResponse> {
|
||||||
return repository.getRecommendLive(
|
return repository.getRecommendLive(
|
||||||
isBlocked = {
|
memberId = member?.id,
|
||||||
if (member != null) {
|
|
||||||
isBlockedBetweenMembers(memberId = member.id!!, creatorId = it)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isAdult = member?.auth != null
|
isAdult = member?.auth != null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import kr.co.vividnext.sodalive.point.MemberPointRepository
|
|||||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||||
import kr.co.vividnext.sodalive.utils.generatePassword
|
import kr.co.vividnext.sodalive.utils.generatePassword
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.cache.CacheManager
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||||
@@ -107,6 +108,7 @@ class MemberService(
|
|||||||
private val countryContext: CountryContext,
|
private val countryContext: CountryContext,
|
||||||
|
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val cacheManager: CacheManager,
|
||||||
|
|
||||||
@Value("\${cloud.aws.s3.bucket}")
|
@Value("\${cloud.aws.s3.bucket}")
|
||||||
private val s3Bucket: String,
|
private val s3Bucket: String,
|
||||||
@@ -117,6 +119,8 @@ class MemberService(
|
|||||||
|
|
||||||
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
|
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
|
||||||
|
|
||||||
|
private val recommendLiveCacheKeyPrefix = "getRecommendLive:"
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun signUpV2(request: SignUpRequestV2): SignUpResponse {
|
fun signUpV2(request: SignUpRequestV2): SignUpResponse {
|
||||||
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
|
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
|
||||||
@@ -558,6 +562,9 @@ class MemberService(
|
|||||||
blockMember.isActive = true
|
blockMember.isActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
evictRecommendLiveCache(memberId)
|
||||||
|
blockTargetMemberIds.forEach { evictRecommendLiveCache(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -570,6 +577,9 @@ class MemberService(
|
|||||||
if (blockMember != null) {
|
if (blockMember != null) {
|
||||||
blockMember.isActive = false
|
blockMember.isActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
evictRecommendLiveCache(memberId)
|
||||||
|
evictRecommendLiveCache(request.blockMemberId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
|
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
|
||||||
@@ -829,6 +839,10 @@ class MemberService(
|
|||||||
return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() }
|
return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun evictRecommendLiveCache(memberId: Long) {
|
||||||
|
cacheManager.getCache("cache_ttl_3_hours")?.evict(recommendLiveCacheKeyPrefix + memberId)
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? {
|
fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? {
|
||||||
val member = repository.findByIdOrNull(id = memberId)
|
val member = repository.findByIdOrNull(id = memberId)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package kr.co.vividnext.sodalive.live.recommend
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||||
|
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
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.context.annotation.Import
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
|
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||||
|
@Import(QueryDslConfig::class)
|
||||||
|
class LiveRecommendRepositoryTest @Autowired constructor(
|
||||||
|
private val queryFactory: JPAQueryFactory,
|
||||||
|
private val memberRepository: MemberRepository,
|
||||||
|
private val blockMemberRepository: BlockMemberRepository,
|
||||||
|
private val recommendLiveCreatorBannerRepository: RecommendLiveCreatorBannerRepository,
|
||||||
|
private val entityManager: EntityManager
|
||||||
|
) {
|
||||||
|
private lateinit var liveRecommendRepository: LiveRecommendRepository
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
liveRecommendRepository = LiveRecommendRepository(queryFactory, "https://cdn.test")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldExcludeBlockedCreatorsInBothDirections() {
|
||||||
|
val viewer = saveMember(nickname = "viewer", role = MemberRole.USER)
|
||||||
|
val creatorBlockedByViewer = saveMember(nickname = "creator-blocked-by-viewer", role = MemberRole.CREATOR)
|
||||||
|
val creatorBlockingViewer = saveMember(nickname = "creator-blocking-viewer", role = MemberRole.CREATOR)
|
||||||
|
val creatorAllowed = saveMember(nickname = "creator-allowed", role = MemberRole.CREATOR)
|
||||||
|
|
||||||
|
saveBanner(creator = creatorBlockedByViewer, order = 1)
|
||||||
|
saveBanner(creator = creatorBlockingViewer, order = 2)
|
||||||
|
saveBanner(creator = creatorAllowed, order = 3)
|
||||||
|
|
||||||
|
saveBlock(member = viewer, blockedMember = creatorBlockedByViewer, isActive = true)
|
||||||
|
saveBlock(member = creatorBlockingViewer, blockedMember = viewer, isActive = true)
|
||||||
|
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true)
|
||||||
|
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals(creatorAllowed.id, result[0].creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldKeepCreatorWhenBlockRelationIsInactive() {
|
||||||
|
val viewer = saveMember(nickname = "viewer-inactive", role = MemberRole.USER)
|
||||||
|
val creator = saveMember(nickname = "creator-inactive", role = MemberRole.CREATOR)
|
||||||
|
|
||||||
|
saveBanner(creator = creator, order = 1)
|
||||||
|
saveBlock(member = viewer, blockedMember = creator, isActive = false)
|
||||||
|
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
val result = liveRecommendRepository.getRecommendLive(memberId = viewer.id, isAdult = true)
|
||||||
|
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals(creator.id, result[0].creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||||
|
return memberRepository.saveAndFlush(
|
||||||
|
Member(
|
||||||
|
email = "$nickname@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = nickname,
|
||||||
|
role = role
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveBanner(creator: Member, order: Int) {
|
||||||
|
val banner = RecommendLiveCreatorBanner(
|
||||||
|
startDate = LocalDateTime.now().minusDays(1),
|
||||||
|
endDate = LocalDateTime.now().plusDays(1),
|
||||||
|
isAdult = false,
|
||||||
|
orders = order,
|
||||||
|
image = "recommend/$order.png"
|
||||||
|
)
|
||||||
|
banner.creator = creator
|
||||||
|
recommendLiveCreatorBannerRepository.saveAndFlush(banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveBlock(member: Member, blockedMember: Member, isActive: Boolean) {
|
||||||
|
val block = BlockMember(isActive = isActive)
|
||||||
|
block.member = member
|
||||||
|
block.blockedMember = blockedMember
|
||||||
|
blockMemberRepository.saveAndFlush(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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 org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
|
||||||
|
class LiveRecommendServiceTest {
|
||||||
|
private lateinit var repository: LiveRecommendRepository
|
||||||
|
private lateinit var blockMemberRepository: BlockMemberRepository
|
||||||
|
private lateinit var service: LiveRecommendService
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
repository = Mockito.mock(LiveRecommendRepository::class.java)
|
||||||
|
blockMemberRepository = Mockito.mock(BlockMemberRepository::class.java)
|
||||||
|
service = LiveRecommendService(repository, blockMemberRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldDelegateToRepositoryWithAdultFlagWhenMemberIsAuthenticated() {
|
||||||
|
val member = Member(
|
||||||
|
email = "member@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "member"
|
||||||
|
)
|
||||||
|
member.id = 10L
|
||||||
|
|
||||||
|
val auth = Auth(
|
||||||
|
name = "name",
|
||||||
|
birth = "19900101",
|
||||||
|
uniqueCi = "ci",
|
||||||
|
di = "di",
|
||||||
|
gender = 1
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
val result = service.getRecommendLive(member)
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
Mockito.verify(repository).getRecommendLive(memberId = member.id, isAdult = true)
|
||||||
|
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)
|
||||||
|
|
||||||
|
val result = service.getRecommendLive(null)
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
Mockito.verify(repository).getRecommendLive(memberId = null, isAdult = false)
|
||||||
|
Mockito.verifyNoInteractions(blockMemberRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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.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 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,
|
||||||
|
authRepository = authRepository,
|
||||||
|
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(),
|
||||||
|
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)
|
||||||
|
|
||||||
|
Mockito.verify(cache).evict("getRecommendLive:$memberId")
|
||||||
|
Mockito.verify(cache).evict("getRecommendLive:$blockedMemberId")
|
||||||
|
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)
|
||||||
|
Mockito.verify(cache).evict("getRecommendLive:$memberId")
|
||||||
|
Mockito.verify(cache).evict("getRecommendLive:$blockedMemberId")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <reified T> mock(): T {
|
||||||
|
return Mockito.mock(T::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user