test #426
@@ -4,7 +4,6 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacade
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
@@ -13,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty(name = ["creator-channel.donation-tab.enabled"], havingValue = "true")
|
||||
@RequestMapping("/api/v2/creator-channels")
|
||||
class CreatorChannelDonationController(
|
||||
private val creatorChannelDonationFacade: CreatorChannelDonationFacade
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.donation.application
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonation
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicy
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationQueryPort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingPort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRecord
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class CreatorChannelDonationQueryService {
|
||||
@Transactional(readOnly = true)
|
||||
class CreatorChannelDonationQueryService(
|
||||
private val queryPortProvider: ObjectProvider<CreatorChannelDonationQueryPort>,
|
||||
private val rankingPort: CreatorChannelDonationRankingPort,
|
||||
private val queryPolicy: CreatorChannelDonationQueryPolicy,
|
||||
private val messageSource: SodaMessageSource,
|
||||
private val langContext: LangContext,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) {
|
||||
fun getDonationTab(
|
||||
creatorId: Long,
|
||||
viewer: Member,
|
||||
@@ -15,6 +41,73 @@ class CreatorChannelDonationQueryService {
|
||||
size: Int?,
|
||||
now: LocalDateTime
|
||||
): CreatorChannelDonationTab {
|
||||
throw SodaException(messageKey = "common.error.invalid_request")
|
||||
val donationPage = queryPolicy.createPage(page, size)
|
||||
val queryPort = queryPortProvider.getObject()
|
||||
val viewerId = viewer.id!!
|
||||
val creator = queryPort.findCreator(creatorId, viewerId)
|
||||
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
||||
|
||||
if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
|
||||
val messageTemplate = messageSource
|
||||
.getMessage("explorer.creator.blocked_access", langContext.lang)
|
||||
.orEmpty()
|
||||
throw SodaException(message = String.format(messageTemplate, creator.nickname))
|
||||
}
|
||||
|
||||
validateCreatorRole(creator)
|
||||
|
||||
val fetchedDonations = queryPort.findChannelDonations(
|
||||
creatorId = creatorId,
|
||||
viewerId = viewerId,
|
||||
now = now,
|
||||
offset = donationPage.offset,
|
||||
limit = donationPage.fetchLimit
|
||||
)
|
||||
|
||||
return CreatorChannelDonationTab(
|
||||
donationCount = queryPort.countChannelDonations(creatorId, viewerId, now),
|
||||
rankings = findRankings(creator, viewerId),
|
||||
donations = queryPolicy.limitItems(fetchedDonations, donationPage).map { it.toDomain() },
|
||||
page = donationPage,
|
||||
hasNext = queryPolicy.hasNext(fetchedDonations, donationPage)
|
||||
)
|
||||
}
|
||||
|
||||
private fun validateCreatorRole(creator: CreatorChannelDonationCreatorRecord) {
|
||||
when (creator.role) {
|
||||
MemberRole.CREATOR -> return
|
||||
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||
}
|
||||
}
|
||||
|
||||
private fun findRankings(
|
||||
creator: CreatorChannelDonationCreatorRecord,
|
||||
viewerId: Long
|
||||
): List<CreatorChannelDonationRanking> {
|
||||
val isViewerCreator = viewerId == creator.creatorId
|
||||
if (!isViewerCreator && !creator.isVisibleDonationRank) return emptyList()
|
||||
|
||||
return rankingPort.findTopRankings(
|
||||
creatorId = creator.creatorId,
|
||||
period = creator.donationRankingPeriod ?: DonationRankingPeriod.CUMULATIVE,
|
||||
withDonationCan = isViewerCreator
|
||||
).map { it.toDomain() }
|
||||
}
|
||||
|
||||
private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation(
|
||||
nickname = nickname.removeDeletedNicknamePrefix(),
|
||||
profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
|
||||
can = can,
|
||||
message = message.orEmpty(),
|
||||
createdAt = createdAt
|
||||
)
|
||||
|
||||
private fun CreatorChannelDonationRankingRecord.toDomain() = CreatorChannelDonationRanking(
|
||||
userId = userId,
|
||||
nickname = nickname,
|
||||
profileImage = profileImage,
|
||||
donationCan = donationCan
|
||||
)
|
||||
|
||||
private fun defaultProfileImageUrl(): String = "$cloudFrontHost/profile/default-profile.png"
|
||||
}
|
||||
|
||||
@@ -10,13 +10,10 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.Crea
|
||||
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationResponse
|
||||
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse
|
||||
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.MemberDonationRankingResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
@@ -28,7 +25,6 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
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
|
||||
@@ -38,7 +34,6 @@ import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@WebMvcTest(CreatorChannelDonationController::class)
|
||||
@Import(CreatorChannelDonationControllerTest.TestSecurityConfig::class)
|
||||
@TestPropertySource(properties = ["creator-channel.donation-tab.enabled=true"])
|
||||
class CreatorChannelDonationControllerTest @Autowired constructor(
|
||||
private val mockMvc: MockMvc
|
||||
) {
|
||||
@@ -71,17 +66,6 @@ class CreatorChannelDonationControllerTest @Autowired constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 채널 후원 탭 controller는 Phase 2 완료 전 기본 등록되지 않도록 property로 보호된다")
|
||||
fun shouldProtectDonationControllerWithFeatureProperty() {
|
||||
val condition = CreatorChannelDonationController::class.java.getAnnotation(ConditionalOnProperty::class.java)
|
||||
|
||||
assertNotNull(condition)
|
||||
assertEquals("creator-channel.donation-tab.enabled", condition.name.first())
|
||||
assertEquals("true", condition.havingValue)
|
||||
assertEquals(false, condition.matchIfMissing)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 채널 후원 탭 조회는 비회원 요청을 거부한다")
|
||||
fun shouldRejectAnonymousCreatorChannelDonationRequest() {
|
||||
|
||||
@@ -1,31 +1,302 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.donation.application
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicy
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationQueryPort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingPort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRecord
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class CreatorChannelDonationQueryServiceTest {
|
||||
@Test
|
||||
@DisplayName("후원 탭 query service placeholder는 내부 예외 대신 명시적인 API 오류를 던진다")
|
||||
fun shouldThrowSodaExceptionUntilPhase2Implementation() {
|
||||
val service = CreatorChannelDonationQueryService()
|
||||
@DisplayName("조회 대상 회원이 없으면 user_not_found 예외를 던진다")
|
||||
fun shouldThrowUserNotFoundWhenCreatorMissing() {
|
||||
val queryPort = FakeDonationQueryPort(creator = null)
|
||||
val service = createService(queryPort = queryPort)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.getDonationTab(
|
||||
creatorId = 1L,
|
||||
viewer = createMember(id = 10L),
|
||||
creatorId = CREATOR_ID,
|
||||
viewer = createMember(VIEWER_ID),
|
||||
page = 0,
|
||||
size = 20,
|
||||
now = LocalDateTime.of(2026, 6, 22, 3, 0)
|
||||
now = NOW
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||
assertEquals("member.validation.user_not_found", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회 대상 회원이 크리에이터가 아니면 creator_not_found 예외를 던진다")
|
||||
fun shouldThrowCreatorNotFoundWhenMemberIsNotCreator() {
|
||||
val queryPort = FakeDonationQueryPort(
|
||||
creator = createCreator(role = MemberRole.USER)
|
||||
)
|
||||
val service = createService(queryPort = queryPort)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.getDonationTab(
|
||||
creatorId = CREATOR_ID,
|
||||
viewer = createMember(VIEWER_ID),
|
||||
page = 0,
|
||||
size = 20,
|
||||
now = NOW
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("member.validation.creator_not_found", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회자와 크리에이터 사이 차단 관계가 있으면 차단 메시지 예외를 던진다")
|
||||
fun shouldThrowBlockedAccessMessageWhenBlocked() {
|
||||
val queryPort = FakeDonationQueryPort(blocked = true)
|
||||
val service = createService(queryPort = queryPort)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.getDonationTab(
|
||||
creatorId = CREATOR_ID,
|
||||
viewer = createMember(VIEWER_ID),
|
||||
page = 0,
|
||||
size = 20,
|
||||
now = NOW
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("creator-nickname님의 요청으로 채널 접근이 제한됩니다.", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("페이지 보정값으로 목록을 조회하고 응답 목록과 hasNext를 조립한다")
|
||||
fun shouldUseResolvedPageForDonationQueryAndLimitResponseItems() {
|
||||
val queryPort = FakeDonationQueryPort(
|
||||
donations = (1..21).map {
|
||||
createDonationRecord(nickname = "donor$it", message = "message$it")
|
||||
},
|
||||
donationCount = 30
|
||||
)
|
||||
val service = createService(queryPort = queryPort)
|
||||
|
||||
val tab = service.getDonationTab(
|
||||
creatorId = CREATOR_ID,
|
||||
viewer = createMember(VIEWER_ID),
|
||||
page = -1,
|
||||
size = 10,
|
||||
now = NOW
|
||||
)
|
||||
|
||||
assertEquals(0L, queryPort.lastFindDonationRequest?.offset)
|
||||
assertEquals(21, queryPort.lastFindDonationRequest?.limit)
|
||||
assertEquals(CREATOR_ID, queryPort.lastCountDonationRequest?.creatorId)
|
||||
assertEquals(VIEWER_ID, queryPort.lastCountDonationRequest?.viewerId)
|
||||
assertEquals(NOW, queryPort.lastCountDonationRequest?.now)
|
||||
assertEquals(30, tab.donationCount)
|
||||
assertEquals(20, tab.donations.size)
|
||||
assertEquals("donor1", tab.donations.first().nickname)
|
||||
assertEquals("message1", tab.donations.first().message)
|
||||
assertEquals(0, tab.page.page)
|
||||
assertEquals(20, tab.page.size)
|
||||
assertEquals(true, tab.hasNext)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("후원 목록은 닉네임, 프로필 이미지, 메시지를 도메인 응답 값으로 변환한다")
|
||||
fun shouldMapDonationRecordsToDomainValues() {
|
||||
val queryPort = FakeDonationQueryPort(
|
||||
donations = listOf(
|
||||
createDonationRecord(
|
||||
nickname = "deleted_donor",
|
||||
profileImagePath = "profile/donor.png",
|
||||
message = null
|
||||
),
|
||||
createDonationRecord(
|
||||
nickname = "default-image-donor",
|
||||
profileImagePath = null,
|
||||
message = "thanks"
|
||||
)
|
||||
)
|
||||
)
|
||||
val service = createService(queryPort = queryPort)
|
||||
|
||||
val tab = service.getDonationTab(
|
||||
creatorId = CREATOR_ID,
|
||||
viewer = createMember(VIEWER_ID),
|
||||
page = 0,
|
||||
size = 20,
|
||||
now = NOW
|
||||
)
|
||||
|
||||
assertEquals("donor", tab.donations[0].nickname)
|
||||
assertEquals("https://cdn.test/profile/donor.png", tab.donations[0].profileImageUrl)
|
||||
assertEquals("", tab.donations[0].message)
|
||||
assertEquals("default-image-donor", tab.donations[1].nickname)
|
||||
assertEquals("https://cdn.test/profile/default-profile.png", tab.donations[1].profileImageUrl)
|
||||
assertEquals("thanks", tab.donations[1].message)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("조회자가 크리에이터 본인이면 순위 공개 여부와 무관하게 donationCan 포함 랭킹을 조회한다")
|
||||
fun shouldFetchRankingsWithDonationCanForCreatorViewer() {
|
||||
val queryPort = FakeDonationQueryPort(
|
||||
creator = createCreator(isVisibleDonationRank = false, donationRankingPeriod = DonationRankingPeriod.WEEKLY)
|
||||
)
|
||||
val rankingPort = FakeDonationRankingPort()
|
||||
val service = createService(queryPort = queryPort, rankingPort = rankingPort)
|
||||
|
||||
val tab = service.getDonationTab(
|
||||
creatorId = CREATOR_ID,
|
||||
viewer = createMember(CREATOR_ID),
|
||||
page = 0,
|
||||
size = 20,
|
||||
now = NOW
|
||||
)
|
||||
|
||||
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.WEEKLY, true), rankingPort.requests.single())
|
||||
assertEquals(createRankingRecord(), rankingPort.records.single())
|
||||
assertEquals(tab.rankings.single().userId, rankingPort.records.single().userId)
|
||||
assertEquals(tab.rankings.single().nickname, rankingPort.records.single().nickname)
|
||||
assertEquals(tab.rankings.single().profileImage, rankingPort.records.single().profileImage)
|
||||
assertEquals(tab.rankings.single().donationCan, rankingPort.records.single().donationCan)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("일반 조회자는 공개 랭킹을 크리에이터 설정 기간과 donationCan 제외 조건으로 조회한다")
|
||||
fun shouldFetchVisibleRankingsForNonCreatorViewerWithConfiguredPeriod() {
|
||||
val weeklyRankingPort = FakeDonationRankingPort()
|
||||
createService(
|
||||
queryPort = FakeDonationQueryPort(
|
||||
creator = createCreator(
|
||||
isVisibleDonationRank = true,
|
||||
donationRankingPeriod = DonationRankingPeriod.WEEKLY
|
||||
)
|
||||
),
|
||||
rankingPort = weeklyRankingPort
|
||||
).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
|
||||
|
||||
val cumulativeRankingPort = FakeDonationRankingPort()
|
||||
createService(
|
||||
queryPort = FakeDonationQueryPort(
|
||||
creator = createCreator(
|
||||
isVisibleDonationRank = true,
|
||||
donationRankingPeriod = DonationRankingPeriod.CUMULATIVE
|
||||
)
|
||||
),
|
||||
rankingPort = cumulativeRankingPort
|
||||
).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
|
||||
|
||||
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.WEEKLY, false), weeklyRankingPort.requests.single())
|
||||
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.CUMULATIVE, false), cumulativeRankingPort.requests.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 랭킹 기간이 없으면 누적 랭킹으로 조회한다")
|
||||
fun shouldUseCumulativeRankingPeriodWhenCreatorPeriodIsNull() {
|
||||
val rankingPort = FakeDonationRankingPort()
|
||||
val service = createService(
|
||||
queryPort = FakeDonationQueryPort(
|
||||
creator = createCreator(isVisibleDonationRank = true, donationRankingPeriod = null)
|
||||
),
|
||||
rankingPort = rankingPort
|
||||
)
|
||||
|
||||
service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
|
||||
|
||||
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.CUMULATIVE, false), rankingPort.requests.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("일반 조회자에게 랭킹이 비공개이면 랭킹 조회 없이 후원 탭 본문을 조립한다")
|
||||
fun shouldSkipRankingsWhenHiddenFromNonCreatorViewer() {
|
||||
val queryPort = FakeDonationQueryPort(
|
||||
creator = createCreator(isVisibleDonationRank = false),
|
||||
donationCount = 2,
|
||||
donations = listOf(
|
||||
createDonationRecord(nickname = "donor1"),
|
||||
createDonationRecord(nickname = "donor2")
|
||||
)
|
||||
)
|
||||
val rankingPort = FakeDonationRankingPort()
|
||||
val service = createService(queryPort = queryPort, rankingPort = rankingPort)
|
||||
|
||||
val tab = service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
|
||||
|
||||
assertEquals(emptyList<RankingRequest>(), rankingPort.requests)
|
||||
assertEquals(emptyList<Any>(), tab.rankings)
|
||||
assertEquals(2, tab.donationCount)
|
||||
assertEquals(2, tab.donations.size)
|
||||
assertEquals(0, tab.page.page)
|
||||
assertEquals(20, tab.page.size)
|
||||
assertFalse(tab.hasNext)
|
||||
}
|
||||
|
||||
private fun createService(
|
||||
queryPort: FakeDonationQueryPort = FakeDonationQueryPort(),
|
||||
rankingPort: FakeDonationRankingPort = FakeDonationRankingPort()
|
||||
): CreatorChannelDonationQueryService {
|
||||
val provider = Mockito.mock(ObjectProvider::class.java) as ObjectProvider<CreatorChannelDonationQueryPort>
|
||||
Mockito.doReturn(queryPort).`when`(provider).getObject()
|
||||
return CreatorChannelDonationQueryService(
|
||||
queryPortProvider = provider,
|
||||
rankingPort = rankingPort,
|
||||
queryPolicy = CreatorChannelDonationQueryPolicy(),
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
cloudFrontHost = "https://cdn.test"
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCreator(
|
||||
role: MemberRole = MemberRole.CREATOR,
|
||||
isVisibleDonationRank: Boolean = true,
|
||||
donationRankingPeriod: DonationRankingPeriod? = DonationRankingPeriod.CUMULATIVE
|
||||
): CreatorChannelDonationCreatorRecord {
|
||||
return CreatorChannelDonationCreatorRecord(
|
||||
creatorId = CREATOR_ID,
|
||||
role = role,
|
||||
nickname = "creator-nickname",
|
||||
isVisibleDonationRank = isVisibleDonationRank,
|
||||
donationRankingPeriod = donationRankingPeriod
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDonationRecord(
|
||||
nickname: String = "donor",
|
||||
profileImagePath: String? = "profile/donor.png",
|
||||
can: Int = 100,
|
||||
message: String? = "thanks",
|
||||
createdAt: LocalDateTime = NOW
|
||||
): CreatorChannelDonationRecord {
|
||||
return CreatorChannelDonationRecord(
|
||||
nickname = nickname,
|
||||
profileImagePath = profileImagePath,
|
||||
can = can,
|
||||
message = message,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRankingRecord(): CreatorChannelDonationRankingRecord {
|
||||
return CreatorChannelDonationRankingRecord(
|
||||
userId = VIEWER_ID,
|
||||
nickname = "fan",
|
||||
profileImage = "https://cdn.test/fan.png",
|
||||
donationCan = 300
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long): Member {
|
||||
@@ -36,4 +307,100 @@ class CreatorChannelDonationQueryServiceTest {
|
||||
role = MemberRole.USER
|
||||
).apply { this.id = id }
|
||||
}
|
||||
|
||||
private class FakeDonationQueryPort(
|
||||
private val creator: CreatorChannelDonationCreatorRecord? = defaultCreator(),
|
||||
private val blocked: Boolean = false,
|
||||
private val donationCount: Int = 0,
|
||||
private val donations: List<CreatorChannelDonationRecord> = emptyList()
|
||||
) : CreatorChannelDonationQueryPort {
|
||||
var lastCountDonationRequest: CountDonationRequest? = null
|
||||
private set
|
||||
var lastFindDonationRequest: FindDonationRequest? = null
|
||||
private set
|
||||
|
||||
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord? {
|
||||
return creator
|
||||
}
|
||||
|
||||
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean {
|
||||
return blocked
|
||||
}
|
||||
|
||||
override fun countChannelDonations(creatorId: Long, viewerId: Long, now: LocalDateTime): Int {
|
||||
lastCountDonationRequest = CountDonationRequest(creatorId, viewerId, now)
|
||||
return donationCount
|
||||
}
|
||||
|
||||
override fun findChannelDonations(
|
||||
creatorId: Long,
|
||||
viewerId: Long,
|
||||
now: LocalDateTime,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelDonationRecord> {
|
||||
lastFindDonationRequest = FindDonationRequest(creatorId, viewerId, now, offset, limit)
|
||||
return donations
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeDonationRankingPort(
|
||||
val records: List<CreatorChannelDonationRankingRecord> = listOf(defaultRankingRecord())
|
||||
) : CreatorChannelDonationRankingPort {
|
||||
val requests = mutableListOf<RankingRequest>()
|
||||
|
||||
override fun findTopRankings(
|
||||
creatorId: Long,
|
||||
period: DonationRankingPeriod,
|
||||
withDonationCan: Boolean
|
||||
): List<CreatorChannelDonationRankingRecord> {
|
||||
requests += RankingRequest(creatorId, period, withDonationCan)
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
private data class CountDonationRequest(
|
||||
val creatorId: Long,
|
||||
val viewerId: Long,
|
||||
val now: LocalDateTime
|
||||
)
|
||||
|
||||
private data class FindDonationRequest(
|
||||
val creatorId: Long,
|
||||
val viewerId: Long,
|
||||
val now: LocalDateTime,
|
||||
val offset: Long,
|
||||
val limit: Int
|
||||
)
|
||||
|
||||
private data class RankingRequest(
|
||||
val creatorId: Long,
|
||||
val period: DonationRankingPeriod,
|
||||
val withDonationCan: Boolean
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val CREATOR_ID = 1L
|
||||
private const val VIEWER_ID = 10L
|
||||
private val NOW: LocalDateTime = LocalDateTime.of(2026, 6, 22, 3, 0)
|
||||
|
||||
private fun defaultCreator(): CreatorChannelDonationCreatorRecord {
|
||||
return CreatorChannelDonationCreatorRecord(
|
||||
creatorId = CREATOR_ID,
|
||||
role = MemberRole.CREATOR,
|
||||
nickname = "creator-nickname",
|
||||
isVisibleDonationRank = true,
|
||||
donationRankingPeriod = DonationRankingPeriod.CUMULATIVE
|
||||
)
|
||||
}
|
||||
|
||||
private fun defaultRankingRecord(): CreatorChannelDonationRankingRecord {
|
||||
return CreatorChannelDonationRankingRecord(
|
||||
userId = VIEWER_ID,
|
||||
nickname = "fan",
|
||||
profileImage = "https://cdn.test/fan.png",
|
||||
donationCan = 300
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user