From 074042270c4dd15174ef080c7de30e0ac4a2f31e Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 15 Jul 2026 20:53:57 +0900 Subject: [PATCH] =?UTF-8?q?test(live):=20=EC=9C=A0=EB=A3=8C=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=9B=90=EC=9E=90=EC=84=B1=20=ED=9A=8C=EA=B7=80=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LiveReservationServiceIntegrationTest.kt | 93 ++++++++++++++++++- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationServiceIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationServiceIntegrationTest.kt index 888f22fe..93996443 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationServiceIntegrationTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationServiceIntegrationTest.kt @@ -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 + ) }