feat(channel-donation): 채널 후원 기능 추가

This commit is contained in:
2026-02-23 22:46:50 +09:00
parent fa5e65b432
commit 1650ed402c
17 changed files with 890 additions and 2 deletions

View File

@@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import kr.co.vividnext.sodalive.common.SodaExceptionHandler
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.data.domain.PageRequest
import org.springframework.data.web.PageableHandlerMethodArgumentResolver
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver
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.test.web.servlet.setup.MockMvcBuilders
class ChannelDonationControllerTest {
private lateinit var channelDonationService: ChannelDonationService
private lateinit var controller: ChannelDonationController
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setup() {
channelDonationService = Mockito.mock(ChannelDonationService::class.java)
controller = ChannelDonationController(channelDonationService)
mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.setControllerAdvice(SodaExceptionHandler(LangContext(), SodaMessageSource()))
.setCustomArgumentResolvers(
AuthenticationPrincipalArgumentResolver(),
PageableHandlerMethodArgumentResolver()
)
.build()
}
@Test
fun shouldReturnErrorResponseWhenRequesterIsAnonymous() {
mockMvc.perform(
get("/explorer/profile/channel-donation")
.param("creatorId", "1")
.param("page", "0")
.param("size", "5")
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value("로그인 정보를 확인해주세요."))
}
@Test
fun shouldForwardPageableAndMemberToServiceWhenControllerMethodIsCalled() {
val member = createMember(id = 7L, role = MemberRole.USER, nickname = "viewer")
val item = GetChannelDonationListItem(
id = 1001L,
memberId = member.id!!,
nickname = member.nickname,
profileUrl = "https://cdn.test/profile/default-profile.png",
can = 3,
isSecret = false,
message = "3캔을 후원하셨습니다.",
createdAt = "2026-02-23T09:30:00"
)
val response = GetChannelDonationListResponse(totalCount = 1, items = listOf(item))
Mockito.`when`(
channelDonationService.getChannelDonationList(
creatorId = 1L,
member = member,
offset = 10L,
limit = 5L
)
).thenReturn(response)
val apiResponse = controller.getChannelDonationList(
creatorId = 1L,
member = member,
pageable = PageRequest.of(2, 5)
)
assertEquals(true, apiResponse.success)
assertEquals(1, apiResponse.data!!.totalCount)
assertEquals(1001L, apiResponse.data!!.items[0].id)
Mockito.verify(channelDonationService).getChannelDonationList(
creatorId = 1L,
member = member,
offset = 10L,
limit = 5L
)
}
private fun createMember(id: Long, role: MemberRole, nickname: String): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
role = role
)
member.id = id
return member
}
}

View File

@@ -0,0 +1,136 @@
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest
@Import(QueryDslConfig::class)
class ChannelDonationMessageRepositoryTest @Autowired constructor(
private val channelDonationMessageRepository: ChannelDonationMessageRepository,
private val memberRepository: MemberRepository,
private val entityManager: EntityManager
) {
@Test
fun shouldFilterByDateAndSortByCreatedAtAndIdDescForViewer() {
val creator = saveMember(nickname = "creator", role = MemberRole.CREATOR)
val viewer = saveMember(nickname = "viewer", role = MemberRole.USER)
val otherUser = saveMember(nickname = "other", role = MemberRole.USER)
val now = LocalDateTime.now()
val tieTime = now.minusDays(2)
val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
val publicTieFirst = saveMessage(member = otherUser, creator = creator, can = 2, isSecret = false)
val publicTieSecond = saveMessage(member = viewer, creator = creator, can = 3, isSecret = false)
val secretMine = saveMessage(member = viewer, creator = creator, can = 4, isSecret = true)
val secretOther = saveMessage(member = otherUser, creator = creator, can = 5, isSecret = true)
updateCreatedAt(oldPublic.id!!, now.minusMonths(2))
updateCreatedAt(publicTieFirst.id!!, tieTime)
updateCreatedAt(publicTieSecond.id!!, tieTime)
updateCreatedAt(secretMine.id!!, now.minusDays(1))
updateCreatedAt(secretOther.id!!, now.minusHours(12))
entityManager.flush()
entityManager.clear()
val list = channelDonationMessageRepository.getChannelDonationMessageList(
creatorId = creator.id!!,
memberId = viewer.id!!,
isCreator = false,
offset = 0,
limit = 10,
startDateTime = now.minusMonths(1)
)
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
creatorId = creator.id!!,
memberId = viewer.id!!,
isCreator = false,
startDateTime = now.minusMonths(1)
)
assertEquals(3, list.size)
assertEquals(secretMine.id, list[0].id)
assertEquals(publicTieSecond.id, list[1].id)
assertEquals(publicTieFirst.id, list[2].id)
assertEquals(3, totalCount)
}
@Test
fun shouldIncludeAllRecentSecretMessagesForCreator() {
val creator = saveMember(nickname = "creator2", role = MemberRole.CREATOR)
val viewer = saveMember(nickname = "viewer2", role = MemberRole.USER)
val otherUser = saveMember(nickname = "other2", role = MemberRole.USER)
val now = LocalDateTime.now()
val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
val recentPublic = saveMessage(member = viewer, creator = creator, can = 2, isSecret = false)
val recentSecretMine = saveMessage(member = viewer, creator = creator, can = 3, isSecret = true)
val recentSecretOther = saveMessage(member = otherUser, creator = creator, can = 4, isSecret = true)
updateCreatedAt(oldPublic.id!!, now.minusMonths(2))
updateCreatedAt(recentPublic.id!!, now.minusDays(3))
updateCreatedAt(recentSecretMine.id!!, now.minusDays(2))
updateCreatedAt(recentSecretOther.id!!, now.minusDays(1))
entityManager.flush()
entityManager.clear()
val list = channelDonationMessageRepository.getChannelDonationMessageList(
creatorId = creator.id!!,
memberId = creator.id!!,
isCreator = true,
offset = 0,
limit = 10,
startDateTime = now.minusMonths(1)
)
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
creatorId = creator.id!!,
memberId = creator.id!!,
isCreator = true,
startDateTime = now.minusMonths(1)
)
assertEquals(3, list.size)
assertEquals(recentSecretOther.id, list[0].id)
assertEquals(recentSecretMine.id, list[1].id)
assertEquals(recentPublic.id, list[2].id)
assertEquals(3, totalCount)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return memberRepository.saveAndFlush(
Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
role = role
)
)
}
private fun saveMessage(member: Member, creator: Member, can: Int, isSecret: Boolean): ChannelDonationMessage {
val message = ChannelDonationMessage(can = can, isSecret = isSecret)
message.member = member
message.creator = creator
return channelDonationMessageRepository.saveAndFlush(message)
}
private fun updateCreatedAt(id: Long, createdAt: LocalDateTime) {
entityManager.createQuery(
"update ChannelDonationMessage m set m.createdAt = :createdAt where m.id = :id"
)
.setParameter("createdAt", createdAt)
.setParameter("id", id)
.executeUpdate()
}
}

View File

@@ -0,0 +1,175 @@
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
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.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import java.time.LocalDateTime
class ChannelDonationServiceTest {
private lateinit var canPaymentService: CanPaymentService
private lateinit var memberRepository: MemberRepository
private lateinit var channelDonationMessageRepository: ChannelDonationMessageRepository
private lateinit var service: ChannelDonationService
@BeforeEach
fun setup() {
canPaymentService = Mockito.mock(CanPaymentService::class.java)
memberRepository = Mockito.mock(MemberRepository::class.java)
channelDonationMessageRepository = Mockito.mock(ChannelDonationMessageRepository::class.java)
service = ChannelDonationService(
canPaymentService = canPaymentService,
memberRepository = memberRepository,
channelDonationMessageRepository = channelDonationMessageRepository,
messageSource = SodaMessageSource(),
langContext = LangContext(),
cloudFrontHost = "https://cdn.test"
)
}
@Test
fun shouldThrowWhenDonateCanIsLessThanOne() {
val member = createMember(id = 10L, role = MemberRole.USER, nickname = "viewer")
val request = PostChannelDonationRequest(
creatorId = 1L,
can = 0,
isSecret = false,
message = "",
container = "aos"
)
val exception = assertThrows(SodaException::class.java) {
service.donate(request, member)
}
assertEquals("content.donation.error.minimum_can", exception.messageKey)
}
@Test
fun shouldPassUserVisibilityFlagToRepositoryWhenRequesterIsNotCreator() {
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer")
val message = ChannelDonationMessage(can = 3, isSecret = true, additionalMessage = "응원합니다")
message.id = 1001L
message.member = viewer
message.creator = creator
message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!),
Mockito.eq(false),
anyLocalDateTime()
)
).thenReturn(1)
Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageList(
Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!),
Mockito.eq(false),
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime()
)
).thenReturn(listOf(message))
val result = service.getChannelDonationList(
creatorId = creator.id!!,
member = viewer,
offset = 0,
limit = 5
)
assertEquals(1, result.totalCount)
assertEquals(1, result.items.size)
assertEquals("3캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message)
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!),
Mockito.eq(false),
anyLocalDateTime()
)
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList(
Mockito.eq(creator.id!!),
Mockito.eq(viewer.id!!),
Mockito.eq(false),
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime()
)
}
@Test
fun shouldPassCreatorVisibilityFlagToRepositoryWhenRequesterIsCreatorSelf() {
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!),
Mockito.eq(creator.id!!),
Mockito.eq(true),
anyLocalDateTime()
)
).thenReturn(0)
Mockito.`when`(
channelDonationMessageRepository.getChannelDonationMessageList(
Mockito.eq(creator.id!!),
Mockito.eq(creator.id!!),
Mockito.eq(true),
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime()
)
).thenReturn(emptyList())
service.getChannelDonationList(
creatorId = creator.id!!,
member = creator,
offset = 0,
limit = 5
)
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount(
Mockito.eq(creator.id!!),
Mockito.eq(creator.id!!),
Mockito.eq(true),
anyLocalDateTime()
)
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList(
Mockito.eq(creator.id!!),
Mockito.eq(creator.id!!),
Mockito.eq(true),
Mockito.eq(0L),
Mockito.eq(5L),
anyLocalDateTime()
)
}
private fun createMember(id: Long, role: MemberRole, nickname: String): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
role = role
)
member.id = id
return member
}
private fun anyLocalDateTime(): LocalDateTime {
Mockito.any(LocalDateTime::class.java)
return LocalDateTime.MIN
}
}