test(home): 크리에이터 랭킹 공개 응답 스키마를 검증한다

This commit is contained in:
2026-07-10 14:46:20 +09:00
parent 263202f0f5
commit 61d5178eba

View File

@@ -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.CreatorRankingScoreSpec
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -23,7 +24,13 @@ import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(properties = ["cloud.aws.cloud-front.host=https://cdn.test"])
@SpringBootTest(
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.datasource.url=jdbc:h2:mem:creator-ranking-controller;" +
"MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
]
)
@AutoConfigureMockMvc
@Transactional
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@@ -33,13 +40,20 @@ class CreatorRankingControllerTest @Autowired constructor(
private val entityManager: EntityManager
) {
@Test
@DisplayName("크리에이터 랭킹 조회는 허용된 응답 필드만 반환하고 점수와 기간은 노출하지 않는")
fun shouldReturnCreatorRankingSchemaWithoutScoreAndPeriodFields() {
@DisplayName("크리에이터 랭킹 API는 공개 응답 스키마만 노출하고 내부 스냅샷 필드를 숨긴")
fun shouldReturnOnlyPublicCreatorRankingSchemaThroughEndpoint() {
val creatorOne = saveMember("creator-one", MemberRole.CREATOR)
val creatorTwo = saveMember("creator-two", MemberRole.CREATOR)
saveSnapshot(creatorId = creatorOne.id!!, nickname = "creator-one", profileImageUrl = "profile-one.png", rankNo = 2)
saveSnapshot(creatorId = creatorTwo.id!!, nickname = "creator-two", profileImageUrl = "profile-two.png", rankNo = 1)
saveSnapshot(
creatorId = 1L,
creatorId = creatorOne.id!!,
nickname = "creator-one",
profileImageUrl = "profile-one.png",
finalScore = 100.0
rankNo = 1,
aggregationStartAtUtc = PREVIOUS_START,
aggregationEndAtUtc = PREVIOUS_END,
visibleFromAtUtc = PREVIOUS_VISIBLE
)
entityManager.flush()
entityManager.clear()
@@ -47,23 +61,33 @@ class CreatorRankingControllerTest @Autowired constructor(
mockMvc.perform(get("/api/v2/home/rankings/creators"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.showRankChange").value(false))
.andExpect(jsonPath("$.data.showRankChange").value(true))
.andExpect(jsonPath("$.data.items[0].rank").value(1))
.andExpect(jsonPath("$.data.items[0].rankChange").doesNotExist())
.andExpect(jsonPath("$.data.items[0].isNew").value(false))
.andExpect(jsonPath("$.data.items[0].creatorId").value(1L))
.andExpect(jsonPath("$.data.items[0].nickname").value("creator-one"))
.andExpect(jsonPath("$.data.items[0].profileImageUrl").value("https://cdn.test/profile-one.png"))
.andExpect(jsonPath("$.data.items[0].isNew").value(true))
.andExpect(jsonPath("$.data.items[0].creatorId").value(creatorTwo.id))
.andExpect(jsonPath("$.data.items[0].nickname").value("creator-two"))
.andExpect(jsonPath("$.data.items[0].profileImageUrl").value("https://cdn.test/profile-two.png"))
.andExpect(jsonPath("$.data.items[1].rank").value(2))
.andExpect(jsonPath("$.data.items[1].rankChange").value(-1))
.andExpect(jsonPath("$.data.items[1].isNew").value(false))
.andExpect(jsonPath("$.data.items[1].creatorId").value(creatorOne.id))
.andExpect(jsonPath("$.data.items[1].nickname").value("creator-one"))
.andExpect(jsonPath("$.data.items[1].profileImageUrl").value("https://cdn.test/profile-one.png"))
.andExpect(jsonPath("$.data.items[0].finalScore").doesNotExist())
.andExpect(jsonPath("$.data.items[0].aggregationStartAtUtc").doesNotExist())
.andExpect(jsonPath("$.data.items[0].aggregationEndAtUtc").doesNotExist())
.andExpect(jsonPath("$.data.aggregationStartAtUtc").doesNotExist())
.andExpect(jsonPath("$.data.aggregationEndAtUtc").doesNotExist())
.andExpect(jsonPath("$.data.items[0].rankNo").doesNotExist())
.andExpect(jsonPath("$.data.items[0].scorePolicyVersion").doesNotExist())
.andExpect(jsonPath("$.data.items[0].scoreDetailJson").doesNotExist())
.andExpect(jsonPath("$.data.fallback").doesNotExist())
.andExpect(jsonPath("$.data.finalScore").doesNotExist())
.andExpect(jsonPath("$.data.rankNo").doesNotExist())
.andExpect(jsonPath("$.data.scorePolicyVersion").doesNotExist())
.andExpect(jsonPath("$.data.scoreDetailJson").doesNotExist())
}
@Test
@DisplayName("크리에이터 랭킹 조회는 비회원도 호출 가능하고 빈 랭킹을 성공 응답으로 반환한")
fun shouldReturnEmptyCreatorRankingsForAnonymous() {
@DisplayName("크리에이터 랭킹 API는 비회원도 빈 랭킹을 성공 응답으로 조회할 수 있")
fun shouldReturnEmptySuccessForAnonymous() {
mockMvc.perform(get("/api/v2/home/rankings/creators"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
@@ -73,7 +97,7 @@ class CreatorRankingControllerTest @Autowired constructor(
}
@Test
@DisplayName("크리에이터 랭킹 조회는 인증 회원 id를 전달해 차단 크리에이터 정보를 마스킹한다")
@DisplayName("크리에이터 랭킹 API는 인증 회원과 차단 관계인 크리에이터 정보를 마스킹한다")
fun shouldMaskBlockedCreatorForAuthenticatedMember() {
val viewer = saveMember("ranking-viewer", MemberRole.USER)
val blockedCreator = saveMember("blocked-creator", MemberRole.CREATOR)
@@ -81,7 +105,7 @@ class CreatorRankingControllerTest @Autowired constructor(
creatorId = blockedCreator.id!!,
nickname = blockedCreator.nickname,
profileImageUrl = "blocked-profile.png",
finalScore = 100.0
rankNo = 1
)
saveBlock(member = viewer, blockedMember = blockedCreator)
entityManager.flush()
@@ -97,6 +121,11 @@ class CreatorRankingControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.items[0].creatorId").value(0L))
.andExpect(jsonPath("$.data.items[0].nickname").value(""))
.andExpect(jsonPath("$.data.items[0].profileImageUrl").value("https://cdn.test/profile/default-profile.png"))
.andExpect(jsonPath("$.data.items[0].finalScore").doesNotExist())
.andExpect(jsonPath("$.data.items[0].rankNo").doesNotExist())
.andExpect(jsonPath("$.data.items[0].scorePolicyVersion").doesNotExist())
.andExpect(jsonPath("$.data.items[0].scoreDetailJson").doesNotExist())
.andExpect(jsonPath("$.data.fallback").doesNotExist())
}
private fun saveMember(seed: String, role: MemberRole): Member {
@@ -123,32 +152,34 @@ class CreatorRankingControllerTest @Autowired constructor(
creatorId: Long,
nickname: String,
profileImageUrl: String?,
finalScore: Double
rankNo: Int,
aggregationStartAtUtc: LocalDateTime = START,
aggregationEndAtUtc: LocalDateTime = END,
visibleFromAtUtc: LocalDateTime = VISIBLE
) {
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),
aggregationStartAtUtc = aggregationStartAtUtc,
aggregationEndAtUtc = aggregationEndAtUtc,
visibleFromAtUtc = visibleFromAtUtc,
creatorId = creatorId,
nickname = nickname,
profileImageUrl = profileImageUrl,
finalScore = finalScore,
contentLiveScore = 0.0,
engagementScore = 0.0,
supportScore = 0.0,
fanLoyaltyScore = 0.0,
liveCanAmount = 0,
contentPurchaseCanAmount = 0,
contentLikeCount = 0,
contentCommentCount = 0,
channelDonationCanAmount = 0,
channelDonationCount = 0,
fanTalkCount = 0,
finalFollowerCount = 0,
followIncrease = 0
rankNo = rankNo,
finalScore = (100 - rankNo).toDouble(),
scorePolicyVersion = CreatorRankingScoreSpec.SCORE_POLICY_VERSION,
scoreDetailJson = "{}"
)
)
}
companion object {
private val PREVIOUS_START = LocalDateTime.of(2026, 5, 24, 15, 0)
private val PREVIOUS_END = LocalDateTime.of(2026, 5, 31, 15, 0)
private val PREVIOUS_VISIBLE = LocalDateTime.of(2026, 6, 1, 0, 0)
private val START = LocalDateTime.of(2026, 5, 31, 15, 0)
private val END = LocalDateTime.of(2026, 6, 7, 15, 0)
private val VISIBLE = LocalDateTime.of(2026, 6, 8, 0, 0)
}
}