From 61d5178eba21e002a4b6f7a01423376e047d36dd Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 10 Jul 2026 14:46:20 +0900 Subject: [PATCH] =?UTF-8?q?test(home):=20=ED=81=AC=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=9E=AD=ED=82=B9=20=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=8A=A4=ED=82=A4=EB=A7=88=EB=A5=BC=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/home/CreatorRankingControllerTest.kt | 103 ++++++++++++------ 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt index f53f9eba..2496fe1f 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt @@ -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) + } }