test(creator-channel): 후원 탭 E2E 검증을 추가한다
This commit is contained in:
@@ -469,7 +469,7 @@ data class CreatorChannelDonationRankingRecord(
|
||||
|
||||
### Phase 3: 통합 검증과 회귀 확인
|
||||
|
||||
- [ ] **Task 3.1: 후원 탭 End-to-End 테스트 추가**
|
||||
- [x] **Task 3.1: 후원 탭 End-to-End 테스트 추가**
|
||||
- 파일:
|
||||
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt`
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt`
|
||||
@@ -494,8 +494,11 @@ data class CreatorChannelDonationRankingRecord(
|
||||
- REFACTOR: End-to-End 테스트 fixture helper 중복을 줄이되 테스트 의도를 흐리지 않는 범위에서만 정리한다.
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest`
|
||||
- Expected: `BUILD SUCCESSFUL`
|
||||
- 실행 기록:
|
||||
- E2E: `CreatorChannelDonationEndToEndTest`를 추가한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest` 실행, 기존 Phase 2 wiring으로 `BUILD SUCCESSFUL` 확인.
|
||||
- 검증 범위: 기본 Spring context endpoint 등록, controller-service-repository-legacy ranking 통합, page 범위 밖 응답, page/size 보정, 일반 조회자 비공개 후원/랭킹 숨김, 크리에이터 본인 비공개 후원 및 `donationCan` 노출을 확인.
|
||||
|
||||
- [ ] **Task 3.2: 관련 테스트와 아키텍처 의존 방향 검증**
|
||||
- [x] **Task 3.2: 관련 테스트와 아키텍처 의존 방향 검증**
|
||||
- 파일:
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation`
|
||||
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation`
|
||||
@@ -516,6 +519,12 @@ data class CreatorChannelDonationRankingRecord(
|
||||
- Expected: 별도 feature flag rollout 정책을 유지하기로 문서화한 경우가 아니라면 검색 결과 0건
|
||||
- Run: `./gradlew ktlintCheck`
|
||||
- Expected: `BUILD SUCCESSFUL`
|
||||
- 실행 기록:
|
||||
- 관련 테스트 묶음: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacadeTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.legacy.LegacyCreatorChannelDonationRankingAdapterTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest` 실행, `BUILD SUCCESSFUL` 확인.
|
||||
- 의존 방향: `rg -n "v2\.api" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation` 실행, 검색 결과 0건 확인.
|
||||
- endpoint mapping: `rg -n "class CreatorChannelDonationController|/\{creatorId\}/donations" src/main/kotlin/kr/co/vividnext/sodalive/v2` 실행, controller class와 endpoint mapping 각 1건 확인.
|
||||
- feature flag: `rg -n "ConditionalOnProperty|creator-channel\.donation-tab\.enabled" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation` 실행, 검색 결과 0건 확인.
|
||||
- format: `./gradlew ktlintCheck` 실행, `BUILD SUCCESSFUL` 확인.
|
||||
|
||||
---
|
||||
|
||||
@@ -531,3 +540,5 @@ data class CreatorChannelDonationRankingRecord(
|
||||
## 6. 전체 검증 기록
|
||||
|
||||
- Phase 1 검증은 각 Task 실행 기록에 누적했다.
|
||||
- Phase 2 검증은 각 Task 실행 기록에 누적했다.
|
||||
- Phase 3 검증은 Task 3.1, Task 3.2 실행 기록에 누적했다. 단일 E2E, 관련 테스트 묶음, 의존 방향 검색, endpoint mapping 검색, feature flag 검색, `ktlintCheck` 모두 성공했다.
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.`in`.web
|
||||
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
|
||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage
|
||||
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicy
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest(
|
||||
properties = [
|
||||
"cloud.aws.cloud-front.host=https://cdn.test",
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:creator-channel-donation-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
class CreatorChannelDonationEndToEndTest @Autowired constructor(
|
||||
private val mockMvc: MockMvc,
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionTemplate: TransactionTemplate
|
||||
) {
|
||||
@Test
|
||||
@DisplayName("후원 탭 API는 controller-service-repository를 거쳐 후원 목록과 랭킹을 반환한다")
|
||||
fun shouldReturnDonationTabThroughControllerServiceRepositoryAndLegacyRanking() {
|
||||
val fixture = createFixture("donation-e2e-success", isVisibleDonationRank = true)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/${fixture.creatorId}/donations")
|
||||
.param("page", "0")
|
||||
.param("size", "20")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.donationCount").value(2))
|
||||
.andExpect(jsonPath("$.data.rankings.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.rankings[0].userId").value(fixture.viewer.id!!))
|
||||
.andExpect(jsonPath("$.data.rankings[0].nickname").value(fixture.viewer.nickname))
|
||||
.andExpect(jsonPath("$.data.rankings[0].profileImage").value("https://cdn.test/${fixture.viewer.profileImage}"))
|
||||
.andExpect(jsonPath("$.data.rankings[0].donationCan").value(0))
|
||||
.andExpect(jsonPath("$.data.donations.length()").value(2))
|
||||
.andExpect(jsonPath("$.data.donations[0].nickname").value("donation-e2e-success-viewer"))
|
||||
.andExpect(jsonPath("$.data.donations[0].profileImageUrl").value("https://cdn.test/donation-e2e-success-viewer.png"))
|
||||
.andExpect(jsonPath("$.data.donations[0].can").value(200))
|
||||
.andExpect(jsonPath("$.data.donations[0].message").value("own secret"))
|
||||
.andExpect(jsonPath("$.data.donations[0].createdAtUtc").exists())
|
||||
.andExpect(jsonPath("$.data.donations[1].nickname").value("donation-e2e-success-other"))
|
||||
.andExpect(jsonPath("$.data.donations[1].can").value(100))
|
||||
.andExpect(jsonPath("$.data.donations[1].message").value("public"))
|
||||
.andExpect(jsonPath("$.data.donations[?(@.message == 'hidden')]").isEmpty)
|
||||
.andExpect(jsonPath("$.data.page").value(0))
|
||||
.andExpect(jsonPath("$.data.size").value(20))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("후원 탭 API는 page 범위 밖 요청에 빈 목록과 유지된 count를 반환한다")
|
||||
fun shouldReturnEmptyDonationsAndKeepCountForOutOfRangePage() {
|
||||
val fixture = createFixture("donation-e2e-out-of-range", isVisibleDonationRank = true)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/${fixture.creatorId}/donations")
|
||||
.param("page", "1")
|
||||
.param("size", "20")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.donationCount").value(2))
|
||||
.andExpect(jsonPath("$.data.donations.length()").value(0))
|
||||
.andExpect(jsonPath("$.data.page").value(1))
|
||||
.andExpect(jsonPath("$.data.size").value(20))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("후원 탭 API는 page와 size를 정책 범위로 보정한다")
|
||||
fun shouldClampPageAndSize() {
|
||||
val fixture = createFixture("donation-e2e-clamp", isVisibleDonationRank = true)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/${fixture.creatorId}/donations")
|
||||
.param("page", "-1")
|
||||
.param("size", "100")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.page").value(0))
|
||||
.andExpect(jsonPath("$.data.size").value(50))
|
||||
.andExpect(jsonPath("$.data.donations.length()").value(2))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("후원 랭킹 비공개 크리에이터는 일반 조회자에게 빈 랭킹과 정상 후원 목록을 반환한다")
|
||||
fun shouldReturnEmptyRankingsAndDonationTabForHiddenRankingCreator() {
|
||||
val fixture = createFixture("donation-e2e-hidden-ranking", isVisibleDonationRank = false)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/${fixture.creatorId}/donations")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.rankings.length()").value(0))
|
||||
.andExpect(jsonPath("$.data.donationCount").value(2))
|
||||
.andExpect(jsonPath("$.data.donations.length()").value(2))
|
||||
.andExpect(jsonPath("$.data.page").value(0))
|
||||
.andExpect(jsonPath("$.data.size").value(20))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 본인 조회는 비공개 후원과 실제 donationCan 랭킹을 포함한다")
|
||||
fun shouldReturnPrivateDonationsAndDonationCanForCreatorSelf() {
|
||||
val fixture = createFixture("donation-e2e-self", isVisibleDonationRank = false)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/${fixture.creatorId}/donations")
|
||||
.with(user(MemberAdapter(fixture.creator)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.donationCount").value(3))
|
||||
.andExpect(jsonPath("$.data.rankings.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.rankings[0].userId").value(fixture.viewer.id!!))
|
||||
.andExpect(jsonPath("$.data.rankings[0].donationCan").value(500))
|
||||
.andExpect(jsonPath("$.data.donations.length()").value(3))
|
||||
.andExpect(jsonPath("$.data.donations[?(@.message == 'hidden')]").isNotEmpty)
|
||||
}
|
||||
|
||||
private fun createFixture(prefix: String, isVisibleDonationRank: Boolean): Fixture {
|
||||
return transactionTemplate.execute {
|
||||
val monthStart = CreatorChannelDonationQueryPolicy()
|
||||
.currentKstMonthRange(LocalDateTime.now())
|
||||
.startInclusiveUtc
|
||||
val now = monthStart.plusDays(10)
|
||||
val viewer = saveMember("$prefix-viewer", MemberRole.USER)
|
||||
val creator = saveMember(
|
||||
"$prefix-creator",
|
||||
MemberRole.CREATOR,
|
||||
isVisibleDonationRank = isVisibleDonationRank
|
||||
)
|
||||
val otherDonor = saveMember("$prefix-other", MemberRole.USER)
|
||||
|
||||
saveDonation(creator, otherDonor, 100, now.minusHours(3), additionalMessage = "public")
|
||||
saveDonation(creator, viewer, 200, now.minusHours(2), isSecret = true, additionalMessage = "own secret")
|
||||
saveDonation(creator, otherDonor, 300, now.minusHours(1), isSecret = true, additionalMessage = "hidden")
|
||||
saveRankingDonation(viewer, creator, can = 450, rewardCan = 50)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
Fixture(
|
||||
viewer = viewer,
|
||||
creator = creator,
|
||||
creatorId = creator.id!!
|
||||
)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun saveMember(
|
||||
nickname: String,
|
||||
role: MemberRole,
|
||||
isVisibleDonationRank: Boolean = true
|
||||
): Member {
|
||||
val member = Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "$nickname.png",
|
||||
role = role,
|
||||
isVisibleDonationRank = isVisibleDonationRank,
|
||||
donationRankingPeriod = DonationRankingPeriod.CUMULATIVE
|
||||
)
|
||||
entityManager.persist(member)
|
||||
return member
|
||||
}
|
||||
|
||||
private fun saveDonation(
|
||||
creator: Member,
|
||||
donor: Member,
|
||||
can: Int,
|
||||
createdAt: LocalDateTime,
|
||||
isSecret: Boolean = false,
|
||||
additionalMessage: String
|
||||
): ChannelDonationMessage {
|
||||
val donation = ChannelDonationMessage(can = can, isSecret = isSecret, additionalMessage = additionalMessage)
|
||||
donation.creator = creator
|
||||
donation.member = donor
|
||||
entityManager.persist(donation)
|
||||
entityManager.flush()
|
||||
updateCreatedAt("ChannelDonationMessage", donation.id!!, createdAt)
|
||||
return donation
|
||||
}
|
||||
|
||||
private fun saveRankingDonation(donor: Member, creator: Member, can: Int, rewardCan: Int) {
|
||||
val useCan = UseCan(CanUsage.CHANNEL_DONATION, can = can, rewardCan = rewardCan, isRefund = false)
|
||||
useCan.member = donor
|
||||
entityManager.persist(useCan)
|
||||
|
||||
val calculate = UseCanCalculate(
|
||||
can = can + rewardCan,
|
||||
paymentGateway = PaymentGateway.PG,
|
||||
status = UseCanCalculateStatus.RECEIVED
|
||||
)
|
||||
calculate.useCan = useCan
|
||||
calculate.recipientCreatorId = creator.id
|
||||
entityManager.persist(calculate)
|
||||
}
|
||||
|
||||
private fun updateCreatedAt(entityName: String, id: Long, createdAt: LocalDateTime) {
|
||||
entityManager.createQuery("update $entityName e set e.createdAt = :createdAt where e.id = :id")
|
||||
.setParameter("createdAt", createdAt)
|
||||
.setParameter("id", id)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private data class Fixture(
|
||||
val viewer: Member,
|
||||
val creator: Member,
|
||||
val creatorId: Long
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user