fix(content): 오디오 추천 조회 fallback을 섹션별로 적용한다
This commit is contained in:
@@ -7,12 +7,8 @@ import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommenda
|
|||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendations
|
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendations
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||||
import org.redisson.api.RedissonClient
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Duration
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
||||||
@@ -20,30 +16,31 @@ import java.time.ZoneId
|
|||||||
class AudioRecommendationQueryService(
|
class AudioRecommendationQueryService(
|
||||||
private val queryPort: AudioRecommendationQueryPort,
|
private val queryPort: AudioRecommendationQueryPort,
|
||||||
private val memberContentPreferenceService: MemberContentPreferenceService,
|
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||||
private val snapshotPort: RecommendationSnapshotPort,
|
private val snapshotFallbackService: AudioRecommendationSnapshotFallbackService
|
||||||
private val snapshotRefreshService: AudioRecommendationSnapshotRefreshService,
|
|
||||||
private val redissonClient: RedissonClient
|
|
||||||
) {
|
) {
|
||||||
fun getRecommendations(member: Member?): AudioRecommendations {
|
fun getRecommendations(member: Member?): AudioRecommendations {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
|
val fallbackNow = LocalDateTime.now(KST_ZONE)
|
||||||
val canViewAdultContent = canViewAdultContent(member)
|
val canViewAdultContent = canViewAdultContent(member)
|
||||||
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
||||||
val memberId = member?.id
|
val memberId = member?.id
|
||||||
val newAndHotSectionType = newAndHotSectionType(visibility)
|
val newAndHotSectionType = newAndHotSectionType(visibility)
|
||||||
val newAndHotSnapshots = snapshotPort.findLatestSnapshots(newAndHotSectionType, limit = NEW_AND_HOT_HOME_LIMIT)
|
val mostCommentedSectionType = mostCommentedSectionType(visibility)
|
||||||
val mostCommentedSnapshots = snapshotPort.findLatestSnapshots(
|
val recommendedAudioSectionType = recommendedAudioSectionType(visibility)
|
||||||
mostCommentedSectionType(visibility),
|
val newAndHotSnapshots = findSnapshotsWithFallback(
|
||||||
limit = MOST_COMMENTED_AUDIO_LIMIT
|
|
||||||
)
|
|
||||||
val recommendedSnapshots = snapshotPort.findLatestSnapshots(
|
|
||||||
recommendedAudioSectionType(visibility),
|
|
||||||
limit = RECOMMENDED_AUDIO_LIMIT
|
|
||||||
)
|
|
||||||
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(
|
|
||||||
newAndHotSectionType,
|
newAndHotSectionType,
|
||||||
newAndHotSnapshots,
|
limit = NEW_AND_HOT_HOME_LIMIT,
|
||||||
offset = 0,
|
now = fallbackNow
|
||||||
limit = NEW_AND_HOT_HOME_LIMIT
|
)
|
||||||
|
val mostCommentedSnapshots = findSnapshotsWithFallback(
|
||||||
|
mostCommentedSectionType,
|
||||||
|
limit = MOST_COMMENTED_AUDIO_LIMIT,
|
||||||
|
now = fallbackNow
|
||||||
|
)
|
||||||
|
val recommendedSnapshots = findSnapshotsWithFallback(
|
||||||
|
recommendedAudioSectionType,
|
||||||
|
limit = RECOMMENDED_AUDIO_LIMIT,
|
||||||
|
now = fallbackNow
|
||||||
)
|
)
|
||||||
|
|
||||||
return AudioRecommendations(
|
return AudioRecommendations(
|
||||||
@@ -51,7 +48,7 @@ class AudioRecommendationQueryService(
|
|||||||
originalSeries = queryPort.findOriginalSeries(ORIGINAL_SERIES_LIMIT, memberId, canViewAdultContent, now),
|
originalSeries = queryPort.findOriginalSeries(ORIGINAL_SERIES_LIMIT, memberId, canViewAdultContent, now),
|
||||||
latestAudios = queryPort.findLatestAudios(LATEST_AUDIO_LIMIT, memberId, canViewAdultContent, now),
|
latestAudios = queryPort.findLatestAudios(LATEST_AUDIO_LIMIT, memberId, canViewAdultContent, now),
|
||||||
newAndHotAudios = queryPort.findAudioCardsByIds(
|
newAndHotAudios = queryPort.findAudioCardsByIds(
|
||||||
refreshedNewAndHotSnapshots.map { it.targetId },
|
newAndHotSnapshots.map { it.targetId },
|
||||||
memberId,
|
memberId,
|
||||||
canViewAdultContent,
|
canViewAdultContent,
|
||||||
now
|
now
|
||||||
@@ -74,14 +71,14 @@ class AudioRecommendationQueryService(
|
|||||||
|
|
||||||
fun findNewAndHotAudios(member: Member, offset: Long, limit: Int): List<AudioCard> {
|
fun findNewAndHotAudios(member: Member, offset: Long, limit: Int): List<AudioCard> {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
|
val fallbackNow = LocalDateTime.now(KST_ZONE)
|
||||||
val canViewAdultContent = canViewAdultContent(member)
|
val canViewAdultContent = canViewAdultContent(member)
|
||||||
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
||||||
val sectionType = newAndHotSectionType(visibility)
|
val sectionType = newAndHotSectionType(visibility)
|
||||||
val snapshots = snapshotPort.findLatestSnapshots(sectionType, offset, limit)
|
val snapshots = findSnapshotsWithFallback(sectionType, offset, limit, fallbackNow)
|
||||||
val refreshedSnapshots = refreshMissingNewAndHotSnapshots(sectionType, snapshots, offset, limit)
|
|
||||||
|
|
||||||
return queryPort.findAudioCardsByIds(
|
return queryPort.findAudioCardsByIds(
|
||||||
refreshedSnapshots.map { it.targetId },
|
snapshots.map { it.targetId },
|
||||||
member.id,
|
member.id,
|
||||||
canViewAdultContent,
|
canViewAdultContent,
|
||||||
now
|
now
|
||||||
@@ -113,29 +110,13 @@ class AudioRecommendationQueryService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshMissingNewAndHotSnapshots(
|
private fun findSnapshotsWithFallback(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
snapshots: List<RecommendationSnapshotRecord>,
|
offset: Long = 0,
|
||||||
offset: Long,
|
limit: Int,
|
||||||
limit: Int
|
now: LocalDateTime
|
||||||
): List<RecommendationSnapshotRecord> {
|
): List<RecommendationSnapshotRecord> {
|
||||||
if (snapshots.isNotEmpty()) return snapshots
|
return snapshotFallbackService.refreshIfMissing(sectionType, offset, limit, now)
|
||||||
val today = LocalDate.now(KST_ZONE)
|
|
||||||
val marker = redissonClient.getBucket<String>(newAndHotLazyRefreshMarkerKey(today))
|
|
||||||
if (!marker.setIfAbsent(LAZY_REFRESH_ATTEMPTED_VALUE, LAZY_REFRESH_MARKER_TTL)) {
|
|
||||||
return snapshots
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
snapshotRefreshService.refreshDailySnapshots()
|
|
||||||
}.onFailure { ex ->
|
|
||||||
marker.delete()
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
return snapshotPort.findLatestSnapshots(sectionType, offset, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newAndHotLazyRefreshMarkerKey(date: LocalDate): String {
|
|
||||||
return "$LAZY_REFRESH_MARKER_KEY_PREFIX:$date"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canViewAdultContent(member: Member?): Boolean {
|
private fun canViewAdultContent(member: Member?): Boolean {
|
||||||
@@ -152,9 +133,6 @@ class AudioRecommendationQueryService(
|
|||||||
const val NEW_AND_HOT_HOME_LIMIT = 12
|
const val NEW_AND_HOT_HOME_LIMIT = 12
|
||||||
const val MOST_COMMENTED_AUDIO_LIMIT = 5
|
const val MOST_COMMENTED_AUDIO_LIMIT = 5
|
||||||
const val RECOMMENDED_AUDIO_LIMIT = 20
|
const val RECOMMENDED_AUDIO_LIMIT = 20
|
||||||
private const val LAZY_REFRESH_MARKER_KEY_PREFIX = "audio-recommendation:new-and-hot:lazy-refresh-attempted"
|
|
||||||
private const val LAZY_REFRESH_ATTEMPTED_VALUE = "1"
|
|
||||||
private val LAZY_REFRESH_MARKER_TTL: Duration = Duration.ofDays(2)
|
|
||||||
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
|
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,30 +7,21 @@ import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
|
|||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationVisibility
|
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationVisibility
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.redisson.api.RBucket
|
|
||||||
import org.redisson.api.RedissonClient
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
class AudioRecommendationQueryServiceTest {
|
class AudioRecommendationQueryServiceTest {
|
||||||
private val queryPort = Mockito.mock(AudioRecommendationQueryPort::class.java)
|
private val queryPort = Mockito.mock(AudioRecommendationQueryPort::class.java)
|
||||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||||
private val snapshotPort = Mockito.mock(RecommendationSnapshotPort::class.java)
|
private val fallbackService = Mockito.mock(AudioRecommendationSnapshotFallbackService::class.java)
|
||||||
private val refreshService = Mockito.mock(AudioRecommendationSnapshotRefreshService::class.java)
|
|
||||||
private val redissonClient = Mockito.mock(RedissonClient::class.java)
|
|
||||||
private val lazyRefreshMarker = Mockito.mock(RBucket::class.java) as RBucket<String>
|
|
||||||
private val service = AudioRecommendationQueryService(
|
private val service = AudioRecommendationQueryService(
|
||||||
queryPort,
|
queryPort,
|
||||||
preferenceService,
|
preferenceService,
|
||||||
snapshotPort,
|
fallbackService
|
||||||
refreshService,
|
|
||||||
redissonClient
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -40,8 +31,8 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("조회 서비스는 SAFE 스냅샷을 lazy refresh 후 상세 섹션으로 조립한다")
|
@DisplayName("조회 서비스는 SAFE 스냅샷 기반 3개 섹션을 fallback 경로로 조립한다")
|
||||||
fun shouldBuildRecommendationsFromSafeSnapshotsWithLazyRefresh() {
|
fun shouldBuildRecommendationsFromSafeSnapshotsWithFallback() {
|
||||||
val snapshot = RecommendationSnapshotRecord(
|
val snapshot = RecommendationSnapshotRecord(
|
||||||
sectionType = RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
sectionType = RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
targetId = 1L,
|
targetId = 1L,
|
||||||
@@ -49,39 +40,39 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
snapshotAt = LocalDateTime.now(),
|
snapshotAt = LocalDateTime.now(),
|
||||||
randomTieBreaker = 1.0
|
randomTieBreaker = 1.0
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>(), listOf(snapshot))
|
stubFallback(
|
||||||
.`when`(snapshotPort)
|
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT,
|
||||||
|
listOf(snapshot)
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
stubFallback(
|
||||||
.`when`(snapshotPort)
|
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT
|
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT,
|
||||||
|
emptyList()
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
stubFallback(
|
||||||
.`when`(snapshotPort)
|
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.RECOMMENDED_AUDIO_LIMIT
|
AudioRecommendationQueryService.RECOMMENDED_AUDIO_LIMIT,
|
||||||
|
emptyList()
|
||||||
)
|
)
|
||||||
allowLazyRefreshOnce()
|
|
||||||
|
|
||||||
val recommendations = service.getRecommendations(null)
|
val recommendations = service.getRecommendations(null)
|
||||||
|
|
||||||
assertEquals(0, recommendations.mostCommentedAudios.size)
|
assertEquals(0, recommendations.mostCommentedAudios.size)
|
||||||
Mockito.verify(refreshService).refreshDailySnapshots()
|
verifyFallback(
|
||||||
Mockito.verify(snapshotPort, Mockito.times(1)).findLatestSnapshots(
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
|
0,
|
||||||
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
||||||
|
)
|
||||||
|
verifyFallback(
|
||||||
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT
|
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT
|
||||||
)
|
)
|
||||||
Mockito.verify(snapshotPort, Mockito.times(1)).findLatestSnapshots(
|
verifyFallback(
|
||||||
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.RECOMMENDED_AUDIO_LIMIT
|
AudioRecommendationQueryService.RECOMMENDED_AUDIO_LIMIT
|
||||||
@@ -114,35 +105,34 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("New & Hot lazy refresh는 보강 후에도 비어 있으면 같은 KST 날짜에 다시 실행하지 않는다")
|
@DisplayName("Most Commented만 비어 있어도 해당 섹션 fallback을 독립적으로 호출한다")
|
||||||
fun shouldAttemptEmptyNewAndHotLazyRefreshOncePerKstDate() {
|
fun shouldFallbackMostCommentedIndependentlyWhenNewAndHotExists() {
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
stubFallback(
|
||||||
.`when`(snapshotPort)
|
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT,
|
||||||
|
listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE, 1L))
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
stubFallback(
|
||||||
.`when`(snapshotPort)
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
||||||
.findLatestSnapshots(
|
0,
|
||||||
|
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
stubFallback(
|
||||||
|
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
||||||
|
0,
|
||||||
|
AudioRecommendationQueryService.RECOMMENDED_AUDIO_LIMIT,
|
||||||
|
listOf(snapshot(RecommendedSectionType.RECOMMENDED_AUDIO_SAFE, 2L))
|
||||||
|
)
|
||||||
|
|
||||||
|
service.getRecommendations(null)
|
||||||
|
|
||||||
|
verifyFallback(
|
||||||
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT
|
AudioRecommendationQueryService.MOST_COMMENTED_AUDIO_LIMIT
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
|
||||||
.`when`(snapshotPort)
|
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
|
||||||
0,
|
|
||||||
AudioRecommendationQueryService.RECOMMENDED_AUDIO_LIMIT
|
|
||||||
)
|
|
||||||
allowLazyRefreshOnce()
|
|
||||||
|
|
||||||
service.getRecommendations(null)
|
|
||||||
service.getRecommendations(null)
|
|
||||||
|
|
||||||
Mockito.verify(refreshService, Mockito.times(1)).refreshDailySnapshots()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -150,19 +140,18 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
fun shouldUseStoredPreferenceForMemberAdultVisibility() {
|
fun shouldUseStoredPreferenceForMemberAdultVisibility() {
|
||||||
val member = member(id = 10L)
|
val member = member(id = 10L)
|
||||||
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
||||||
Mockito.doReturn(listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L)))
|
stubFallback(
|
||||||
.`when`(snapshotPort)
|
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT,
|
||||||
|
listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L))
|
||||||
)
|
)
|
||||||
|
|
||||||
service.getRecommendations(member)
|
service.getRecommendations(member)
|
||||||
|
|
||||||
Mockito.verify(preferenceService).canViewAdultContent(member)
|
Mockito.verify(preferenceService).canViewAdultContent(member)
|
||||||
Mockito.verify(preferenceService, Mockito.never()).initializeDefaultPreference(member)
|
Mockito.verify(preferenceService, Mockito.never()).initializeDefaultPreference(member)
|
||||||
Mockito.verify(snapshotPort).findLatestSnapshots(
|
verifyFallback(
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
||||||
@@ -174,16 +163,16 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
fun shouldKeepNewAndHotHomeLimitAtTwelve() {
|
fun shouldKeepNewAndHotHomeLimitAtTwelve() {
|
||||||
val member = member(id = 10L)
|
val member = member(id = 10L)
|
||||||
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
||||||
Mockito.doReturn(listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L))).`when`(snapshotPort)
|
stubFallback(
|
||||||
.findLatestSnapshots(
|
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT,
|
||||||
|
listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L))
|
||||||
)
|
)
|
||||||
|
|
||||||
service.getRecommendations(member)
|
service.getRecommendations(member)
|
||||||
|
|
||||||
Mockito.verify(snapshotPort).findLatestSnapshots(
|
verifyFallback(
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
||||||
@@ -200,15 +189,19 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 5L)
|
snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 5L)
|
||||||
)
|
)
|
||||||
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
||||||
Mockito.doReturn(snapshots).`when`(snapshotPort)
|
stubFallback(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21, snapshots)
|
||||||
.findLatestSnapshots(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21)
|
|
||||||
Mockito.doReturn(listOf(audioCard(3L), audioCard(4L), audioCard(5L))).`when`(queryPort)
|
Mockito.doReturn(listOf(audioCard(3L), audioCard(4L), audioCard(5L))).`when`(queryPort)
|
||||||
.findAudioCardsByIds(eqValue(listOf(3L, 4L, 5L)), eqValue(member.id), eqValue(true), anyLocalDateTime())
|
.findAudioCardsByIds(
|
||||||
|
eqValue(listOf(3L, 4L, 5L)),
|
||||||
|
eqValue(member.id),
|
||||||
|
eqValue(true),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
|
||||||
val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21)
|
val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21)
|
||||||
|
|
||||||
assertEquals(listOf(3L, 4L, 5L), result.map { it.audioContentId })
|
assertEquals(listOf(3L, 4L, 5L), result.map { it.audioContentId })
|
||||||
Mockito.verify(snapshotPort).findLatestSnapshots(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21)
|
verifyFallback(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -280,15 +273,27 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stubFallback(
|
||||||
|
sectionType: RecommendedSectionType,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int,
|
||||||
|
snapshots: List<RecommendationSnapshotRecord>
|
||||||
|
) {
|
||||||
|
Mockito.doReturn(snapshots)
|
||||||
|
.`when`(fallbackService)
|
||||||
|
.refreshIfMissing(eqValue(sectionType), eqValue(offset), eqValue(limit), anyLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyFallback(sectionType: RecommendedSectionType, offset: Long, limit: Int) {
|
||||||
|
Mockito.verify(fallbackService).refreshIfMissing(
|
||||||
|
eqValue(sectionType),
|
||||||
|
eqValue(offset),
|
||||||
|
eqValue(limit),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T> eqValue(value: T): T {
|
private fun <T> eqValue(value: T): T {
|
||||||
return Mockito.eq(value) ?: value
|
return Mockito.eq(value) ?: value
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun allowLazyRefreshOnce() {
|
|
||||||
Mockito.doReturn(lazyRefreshMarker).`when`(redissonClient).getBucket<String>(Mockito.anyString())
|
|
||||||
Mockito.doReturn(true, false).`when`(lazyRefreshMarker).setIfAbsent(
|
|
||||||
eqValue("1"),
|
|
||||||
eqValue(Duration.ofDays(2))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user