test(live): 유료 예약 원자성 회귀 테스트를 추가한다

This commit is contained in:
2026-07-15 20:53:57 +09:00
parent 7167e6b3de
commit 074042270c

View File

@@ -1,15 +1,26 @@
package kr.co.vividnext.sodalive.live.reservation
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.payment.Payment
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.can.use.UseCanRepository
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.AfterEach
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.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.SpyBean
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
@@ -34,9 +45,15 @@ import javax.persistence.EntityManager
class LiveReservationServiceIntegrationTest @Autowired constructor(
private val service: LiveReservationService,
private val repository: LiveReservationRepository,
private val memberRepository: MemberRepository,
private val chargeRepository: ChargeRepository,
private val useCanRepository: UseCanRepository,
private val transactionTemplate: TransactionTemplate,
private val entityManager: EntityManager
) {
@SpyBean
private lateinit var repositorySpy: LiveReservationRepository
@BeforeEach
fun setUpRequestContext() {
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest()))
@@ -73,7 +90,49 @@ class LiveReservationServiceIntegrationTest @Autowired constructor(
assertEquals(fixture.memberId, savedReservation.memberId)
}
private fun createFixture(): Fixture {
@Test
@DisplayName("유료 예약 저장이 실패하면 결제와 예약을 함께 롤백한다")
fun shouldRollbackPaymentAndReservationWhenPaidReservationSaveFails() {
val fixture = createFixture(price = 100)
val useCanCountBefore = useCanRepository.count()
Mockito.doAnswer {
assertEquals(useCanCountBefore + 1, useCanRepository.count())
throw DataIntegrityViolationException("reservation save failed")
}
.`when`(repositorySpy)
.save(Mockito.any(LiveReservation::class.java))
val exception = assertThrows(DataIntegrityViolationException::class.java) {
service.makeReservation(
request = MakeLiveReservationRequest(
roomId = fixture.roomId,
container = "web",
timezone = "Asia/Seoul"
),
memberId = fixture.memberId
)
}
val rollbackState = transactionTemplate.execute {
val member = memberRepository.findById(fixture.memberId).orElseThrow()
val charge = chargeRepository.findById(fixture.chargeId!!).orElseThrow()
RollbackState(
memberCan = member.getChargeCan("web") + member.getRewardCan("web"),
chargeCan = charge.chargeCan + charge.rewardCan,
useCanCount = useCanRepository.count(),
reservationExists = repository.isExistsReservation(fixture.roomId, fixture.memberId)
)
}!!
assertEquals("reservation save failed", exception.message)
assertEquals(fixture.price, rollbackState.memberCan)
assertEquals(fixture.price, rollbackState.chargeCan)
assertEquals(useCanCountBefore, rollbackState.useCanCount)
assertEquals(false, rollbackState.reservationExists)
}
private fun createFixture(price: Int = 0): Fixture {
return transactionTemplate.execute {
val creator = Member(
email = "live-reservation-creator@test.com",
@@ -87,14 +146,29 @@ class LiveReservationServiceIntegrationTest @Autowired constructor(
password = "password",
nickname = "live-reservation-member"
)
member.pgChargeCan = price
entityManager.persist(member)
val charge = if (price > 0) {
Charge(chargeCan = price, rewardCan = 0).also {
it.member = member
it.payment = Payment(
status = PaymentStatus.COMPLETE,
paymentGateway = PaymentGateway.PG
)
entityManager.persist(it)
}
} else {
null
}
val room = LiveRoom(
title = "예약 라이브",
notice = "예약 라이브 안내",
beginDateTime = LocalDateTime.now().plusDays(1),
numberOfPeople = 10,
isAdult = false
isAdult = false,
price = price
)
room.member = creator
entityManager.persist(room)
@@ -102,7 +176,9 @@ class LiveReservationServiceIntegrationTest @Autowired constructor(
entityManager.flush()
val fixture = Fixture(
roomId = room.id!!,
memberId = member.id!!
memberId = member.id!!,
chargeId = charge?.id,
price = price
)
entityManager.clear()
fixture
@@ -111,11 +187,20 @@ class LiveReservationServiceIntegrationTest @Autowired constructor(
private data class Fixture(
val roomId: Long,
val memberId: Long
val memberId: Long,
val chargeId: Long?,
val price: Int
)
private data class SavedReservation(
val roomId: Long,
val memberId: Long
)
private data class RollbackState(
val memberCan: Int,
val chargeCan: Int,
val useCanCount: Long,
val reservationExists: Boolean
)
}