fix(content-ranking): 공개 전 랭킹 조회를 차단한다
This commit is contained in:
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.CreatorRankingSnapshot
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -126,8 +127,10 @@ class CreatorRankingControllerTest @Autowired constructor(
|
||||
) {
|
||||
entityManager.persist(
|
||||
CreatorRankingSnapshot(
|
||||
rankingType = CreatorRankingType.WEEKLY,
|
||||
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0, 0),
|
||||
aggregationEndAtUtc = LocalDateTime.of(2026, 6, 7, 15, 0, 0),
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0, 0),
|
||||
creatorId = creatorId,
|
||||
nickname = nickname,
|
||||
profileImageUrl = profileImageUrl,
|
||||
|
||||
+82
-4
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.ranking.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
||||
@@ -236,6 +237,54 @@ class CreatorRankingQueryServiceTest {
|
||||
assertEquals(listOf(false, false, false, true), result.items.map { it.isNew })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회 서비스는 현재 UTC 시각 기준 최신 공개 스냅샷과 직전 공개 스냅샷으로 순위 변화를 계산한다")
|
||||
fun shouldUseLatestVisibleSnapshotsAndPreviousVisibleSnapshots() {
|
||||
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||
val now = ZonedDateTime.of(2026, 6, 8, 9, 0, 0, 0, ZoneId.of("Asia/Seoul"))
|
||||
snapshotPort.latestSnapshots = listOf(
|
||||
snapshot(creatorId = 2L, finalScore = 300.0),
|
||||
snapshot(creatorId = 1L, finalScore = 200.0)
|
||||
)
|
||||
snapshotPort.previousSnapshots = listOf(
|
||||
snapshot(creatorId = 1L, finalScore = 400.0),
|
||||
snapshot(creatorId = 2L, finalScore = 100.0)
|
||||
)
|
||||
val service = service(snapshotPort = snapshotPort, now = now)
|
||||
|
||||
val result = service.getCreatorRankings(viewerMemberId = null)
|
||||
|
||||
assertEquals(CreatorRankingType.WEEKLY, snapshotPort.latestRankingType)
|
||||
assertEquals(LocalDateTime.of(2026, 6, 8, 0, 0), snapshotPort.latestNowUtc)
|
||||
assertEquals(CreatorRankingType.WEEKLY, snapshotPort.previousRankingType)
|
||||
assertEquals(LocalDateTime.of(2026, 5, 31, 15, 0), snapshotPort.previousCurrentAggregationStartAtUtc)
|
||||
assertEquals(LocalDateTime.of(2026, 6, 8, 0, 0), snapshotPort.previousNowUtc)
|
||||
assertEquals(listOf(1, -1), result.items.map { it.rankChange })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("cold-start fallback은 공개 노출 시각 전이면 원천 집계와 스냅샷 생성 위임을 실행하지 않는다")
|
||||
fun shouldNotUseColdStartFallbackBeforeVisibleFromAt() {
|
||||
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||
val snapshotJobService = Mockito.mock(CreatorRankingSnapshotJobService::class.java)
|
||||
snapshotPort.snapshotTableEmpty = true
|
||||
aggregationPort.candidates = listOf(candidate(creatorId = 1L))
|
||||
val service = service(
|
||||
snapshotPort = snapshotPort,
|
||||
aggregationPort = aggregationPort,
|
||||
snapshotJobService = snapshotJobService,
|
||||
now = ZonedDateTime.of(2026, 6, 8, 8, 59, 59, 0, ZoneId.of("Asia/Seoul"))
|
||||
)
|
||||
|
||||
val result = service.getCreatorRankings(viewerMemberId = null)
|
||||
|
||||
assertFalse(result.showRankChange)
|
||||
assertTrue(result.items.isEmpty())
|
||||
assertEquals(0, aggregationPort.aggregateCallCount)
|
||||
Mockito.verify(snapshotJobService, Mockito.never()).ensureLastCompletedWeekSnapshotForColdStart()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동점 스냅샷은 같은 점수 구간 안에서만 섞이고 상위 20명만 반환한다")
|
||||
fun shouldRandomizeOnlyWithinTieGroupsAndLimitToTwentyItems() {
|
||||
@@ -400,16 +449,15 @@ class CreatorRankingQueryServiceTest {
|
||||
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingQuerySnapshotPort(),
|
||||
blockPort: CreatorRankingBlockPort = FakeCreatorRankingBlockPort(),
|
||||
aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingQueryAggregationPort(),
|
||||
snapshotJobService: CreatorRankingSnapshotJobService = Mockito.mock(CreatorRankingSnapshotJobService::class.java)
|
||||
snapshotJobService: CreatorRankingSnapshotJobService = Mockito.mock(CreatorRankingSnapshotJobService::class.java),
|
||||
now: ZonedDateTime = ZonedDateTime.of(2026, 6, 8, 9, 0, 0, 0, ZoneId.of("Asia/Seoul"))
|
||||
): CreatorRankingQueryService {
|
||||
return CreatorRankingQueryService(
|
||||
snapshotPort = snapshotPort,
|
||||
blockPort = blockPort,
|
||||
aggregationPort = aggregationPort,
|
||||
snapshotJobService = snapshotJobService,
|
||||
nowProvider = {
|
||||
ZonedDateTime.of(2026, 6, 8, 7, 30, 0, 0, ZoneId.of("Asia/Seoul"))
|
||||
},
|
||||
nowProvider = { now },
|
||||
cloudFrontHost = "https://cdn.test"
|
||||
)
|
||||
}
|
||||
@@ -444,8 +492,10 @@ class CreatorRankingQueryServiceTest {
|
||||
finalScore: Double
|
||||
): CreatorRankingSnapshotRecord {
|
||||
return CreatorRankingSnapshotRecord(
|
||||
rankingType = CreatorRankingType.WEEKLY,
|
||||
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0, 0),
|
||||
aggregationEndAtUtc = LocalDateTime.of(2026, 6, 7, 15, 0, 0),
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0, 0),
|
||||
creatorId = creatorId,
|
||||
nickname = "creator-$creatorId",
|
||||
profileImageUrl = "profile-$creatorId.png",
|
||||
@@ -472,6 +522,11 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
||||
var previousSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
||||
var latestFailure: RuntimeException? = null
|
||||
var snapshotTableEmpty: Boolean = true
|
||||
var latestRankingType: CreatorRankingType? = null
|
||||
var latestNowUtc: LocalDateTime? = null
|
||||
var previousRankingType: CreatorRankingType? = null
|
||||
var previousCurrentAggregationStartAtUtc: LocalDateTime? = null
|
||||
var previousNowUtc: LocalDateTime? = null
|
||||
|
||||
override fun findSnapshotsByAggregationPeriod(
|
||||
aggregationStartAtUtc: LocalDateTime,
|
||||
@@ -485,11 +540,34 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
||||
|
||||
override fun findPreviousCompletedSnapshots(): List<CreatorRankingSnapshotRecord> = previousSnapshots
|
||||
|
||||
override fun findLatestVisibleSnapshots(
|
||||
rankingType: CreatorRankingType,
|
||||
nowUtc: LocalDateTime
|
||||
): List<CreatorRankingSnapshotRecord> {
|
||||
latestFailure?.let { throw it }
|
||||
latestRankingType = rankingType
|
||||
latestNowUtc = nowUtc
|
||||
return latestSnapshots
|
||||
}
|
||||
|
||||
override fun findPreviousVisibleSnapshots(
|
||||
rankingType: CreatorRankingType,
|
||||
currentAggregationStartAtUtc: LocalDateTime,
|
||||
nowUtc: LocalDateTime
|
||||
): List<CreatorRankingSnapshotRecord> {
|
||||
previousRankingType = rankingType
|
||||
previousCurrentAggregationStartAtUtc = currentAggregationStartAtUtc
|
||||
previousNowUtc = nowUtc
|
||||
return previousSnapshots
|
||||
}
|
||||
|
||||
override fun isSnapshotTableEmpty(): Boolean = snapshotTableEmpty
|
||||
|
||||
override fun replaceSnapshots(
|
||||
rankingType: CreatorRankingType,
|
||||
aggregationStartAtUtc: LocalDateTime,
|
||||
aggregationEndAtUtc: LocalDateTime,
|
||||
visibleFromAtUtc: LocalDateTime,
|
||||
newSnapshots: List<CreatorRankingSnapshotRecord>
|
||||
) = Unit
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user