diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt index b457778d..496d8387 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -131,6 +131,7 @@ class CanCouponService( } } + @Transactional fun useCanCoupon(couponNumber: String, memberId: Long): String { val member = memberRepository.findByIdOrNull(id = memberId) ?: throw SodaException(messageKey = "common.error.bad_credentials") diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceIntegrationTest.kt new file mode 100644 index 00000000..814afbb9 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceIntegrationTest.kt @@ -0,0 +1,111 @@ +package kr.co.vividnext.sodalive.can.coupon + +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.auth.Auth +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.support.TransactionTemplate +import java.time.LocalDateTime +import javax.persistence.EntityManager + +@SpringBootTest( + properties = [ + "spring.cache.type=none", + "spring.datasource.url=jdbc:h2:mem:can-coupon-service-integration;" + + "MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" + ] +) +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@Transactional(propagation = Propagation.NOT_SUPPORTED) +class CanCouponServiceIntegrationTest @Autowired constructor( + private val service: CanCouponService, + private val memberRepository: MemberRepository, + private val couponNumberRepository: CanCouponNumberRepository, + private val transactionTemplate: TransactionTemplate, + private val entityManager: EntityManager +) { + @Test + @DisplayName("쿠폰 사용은 OSIV off 환경의 실제 서비스 호출 경로에서 lazy auth 접근까지 완료된다") + fun shouldUseCouponThroughTransactionalServiceProxyWhenOpenInViewIsDisabled() { + val fixture = createAuthenticatedMemberAndCouponFixture() + + service.useCanCoupon(couponNumber = fixture.couponNumber, memberId = fixture.memberId) + + val result = transactionTemplate.execute { + val member = memberRepository.findById(fixture.memberId).orElseThrow() + val couponNumber = couponNumberRepository.findByCouponNumber(fixture.couponNumber)!! + + CouponUseResult( + rewardCan = member.pgRewardCan, + usedMemberId = couponNumber.member!!.id!! + ) + }!! + + assertEquals(300, result.rewardCan) + assertEquals(fixture.memberId, result.usedMemberId) + } + + private fun createAuthenticatedMemberAndCouponFixture(): Fixture { + return transactionTemplate.execute { + val member = Member( + email = "can-coupon-osiv@test.com", + password = "password", + nickname = "can-coupon-osiv" + ) + entityManager.persist(member) + + val auth = Auth( + name = "테스트", + birth = "19900101", + uniqueCi = "can-coupon-osiv-ci", + di = "can-coupon-osiv-di", + gender = 1 + ) + auth.member = member + entityManager.persist(auth) + + val coupon = CanCoupon( + couponName = "OSIV 회귀 테스트 쿠폰", + couponType = CouponType.CAN, + can = 300, + couponCount = 1, + validity = LocalDateTime.now().plusDays(1), + isActive = true, + isMultipleUse = false + ) + entityManager.persist(coupon) + + val couponNumber = CanCouponNumber("OSIVCOUPON1234") + couponNumber.canCoupon = coupon + entityManager.persist(couponNumber) + + entityManager.flush() + val fixture = Fixture( + memberId = member.id!!, + couponNumber = couponNumber.couponNumber + ) + entityManager.clear() + fixture + }!! + } + + private data class Fixture( + val memberId: Long, + val couponNumber: String + ) + + private data class CouponUseResult( + val rewardCan: Int, + val usedMemberId: Long + ) +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt index 651ec3fb..392a7aeb 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt @@ -19,6 +19,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.springframework.context.ApplicationEventPublisher +import org.springframework.transaction.annotation.Transactional import java.util.Optional class CanCouponServiceTest { @@ -110,6 +111,20 @@ class CanCouponServiceTest { verify(chargeService, never()).chargeByCoupon("COUPON5678", member) } + @Test + @DisplayName("쿠폰 사용은 OSIV off 환경에서도 lazy 연관을 읽을 수 있도록 트랜잭션 안에서 실행된다") + fun shouldRunUseCanCouponInTransaction() { + val method = CanCouponService::class.java.getDeclaredMethod( + "useCanCoupon", + String::class.java, + java.lang.Long.TYPE + ) + + val transactional = method.getAnnotation(Transactional::class.java) + + assertEquals(false, transactional?.readOnly) + } + private fun createMember(memberId: Long): Member { return Member( email = "member$memberId@test.com",