fix(can-coupon): 쿠폰 사용 본인인증 예외를 성인 노출 정책에 맞춘다
This commit is contained in:
10
docs/20260402_쿠폰사용본인인증예외추가.md
Normal file
10
docs/20260402_쿠폰사용본인인증예외추가.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- [x] `CanCouponService.useCanCoupon`의 기존 본인인증 요구 조건과 국가/성인노출 관련 패턴을 확인한다.
|
||||||
|
- [x] 한국이 아닌 국가에서 `MemberContentPreference.isAdultContentVisibl`가 `true`이면 본인인증 없이 쿠폰 사용이 가능하도록 수정한다.
|
||||||
|
- [x] 변경 파일 진단과 관련 검증을 수행하고 결과를 기록한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 1차 구현
|
||||||
|
- 무엇을: `CanCouponService.useCanCoupon`이 `MemberContentPreferenceService.getStoredPreference(member).isAdult`를 기준으로 쿠폰 사용 가능 여부를 판단하도록 수정하고, 해당 분기 회귀 테스트를 추가했다.
|
||||||
|
- 왜: 한국 사용자는 기존처럼 본인인증이 필요하고, 한국이 아닌 사용자는 성인 노출 설정이 `true`이면 본인인증 없이 쿠폰을 사용할 수 있어야 하기 때문이다.
|
||||||
|
- 어떻게: `./gradlew test --tests "kr.co.vividnext.sodalive.can.coupon.CanCouponServiceTest"` 실행 성공, `./gradlew ktlintCheck` 실행 성공.
|
||||||
@@ -8,6 +8,7 @@ import kr.co.vividnext.sodalive.common.SodaException
|
|||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
@@ -29,6 +30,7 @@ class CanCouponService(
|
|||||||
private val couponNumberRepository: CanCouponNumberRepository,
|
private val couponNumberRepository: CanCouponNumberRepository,
|
||||||
|
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
|
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||||
|
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
@@ -133,7 +135,8 @@ class CanCouponService(
|
|||||||
val member = memberRepository.findByIdOrNull(id = memberId)
|
val member = memberRepository.findByIdOrNull(id = memberId)
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
|
||||||
if (member.auth == null) throw SodaException(messageKey = "can.coupon.auth_required")
|
val viewerContentPreference = memberContentPreferenceService.getStoredPreference(member)
|
||||||
|
if (!viewerContentPreference.isAdult) throw SodaException(messageKey = "can.coupon.auth_required")
|
||||||
|
|
||||||
issueService.validateAvailableUseCoupon(couponNumber, memberId)
|
issueService.validateAvailableUseCoupon(couponNumber, memberId)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeService
|
||||||
|
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.contentpreference.MemberContentPreferenceService
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
|
||||||
|
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.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
class CanCouponServiceTest {
|
||||||
|
private lateinit var issueService: CanCouponIssueService
|
||||||
|
private lateinit var chargeService: ChargeService
|
||||||
|
private lateinit var repository: CanCouponRepository
|
||||||
|
private lateinit var couponNumberRepository: CanCouponNumberRepository
|
||||||
|
private lateinit var memberRepository: MemberRepository
|
||||||
|
private lateinit var memberContentPreferenceService: MemberContentPreferenceService
|
||||||
|
private lateinit var objectMapper: ObjectMapper
|
||||||
|
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||||
|
private lateinit var messageSource: SodaMessageSource
|
||||||
|
private lateinit var langContext: LangContext
|
||||||
|
|
||||||
|
private lateinit var service: CanCouponService
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
issueService = mock(CanCouponIssueService::class.java)
|
||||||
|
chargeService = mock(ChargeService::class.java)
|
||||||
|
repository = mock(CanCouponRepository::class.java)
|
||||||
|
couponNumberRepository = mock(CanCouponNumberRepository::class.java)
|
||||||
|
memberRepository = mock(MemberRepository::class.java)
|
||||||
|
memberContentPreferenceService = mock(MemberContentPreferenceService::class.java)
|
||||||
|
objectMapper = mock(ObjectMapper::class.java)
|
||||||
|
applicationEventPublisher = mock(ApplicationEventPublisher::class.java)
|
||||||
|
messageSource = mock(SodaMessageSource::class.java)
|
||||||
|
langContext = LangContext()
|
||||||
|
|
||||||
|
service = CanCouponService(
|
||||||
|
issueService = issueService,
|
||||||
|
chargeService = chargeService,
|
||||||
|
repository = repository,
|
||||||
|
couponNumberRepository = couponNumberRepository,
|
||||||
|
memberRepository = memberRepository,
|
||||||
|
memberContentPreferenceService = memberContentPreferenceService,
|
||||||
|
objectMapper = objectMapper,
|
||||||
|
applicationEventPublisher = applicationEventPublisher,
|
||||||
|
messageSource = messageSource,
|
||||||
|
langContext = langContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("비한국 사용자는 성인 노출 설정이 true이면 본인인증 없이 쿠폰을 사용할 수 있다")
|
||||||
|
fun shouldUseCouponWithoutAuthForNonKrAdultVisibleMember() {
|
||||||
|
val member = createMember(memberId = 1L)
|
||||||
|
val couponNumber = "COUPON1234"
|
||||||
|
|
||||||
|
`when`(memberRepository.findById(1L)).thenReturn(Optional.of(member))
|
||||||
|
`when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn(
|
||||||
|
ViewerContentPreference(
|
||||||
|
countryCode = "US",
|
||||||
|
isAdultContentVisible = true,
|
||||||
|
contentType = kr.co.vividnext.sodalive.content.ContentType.ALL,
|
||||||
|
isAdult = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`when`(chargeService.chargeByCoupon(couponNumber, member)).thenReturn("charged")
|
||||||
|
|
||||||
|
val result = service.useCanCoupon(couponNumber = couponNumber, memberId = 1L)
|
||||||
|
|
||||||
|
assertEquals("charged", result)
|
||||||
|
verify(issueService).validateAvailableUseCoupon(couponNumber, 1L)
|
||||||
|
verify(chargeService).chargeByCoupon(couponNumber, member)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("한국 사용자는 본인인증이 없으면 기존처럼 쿠폰 사용이 불가능하다")
|
||||||
|
fun shouldThrowWhenAdultPolicyIsFalse() {
|
||||||
|
val member = createMember(memberId = 2L)
|
||||||
|
|
||||||
|
`when`(memberRepository.findById(2L)).thenReturn(Optional.of(member))
|
||||||
|
`when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn(
|
||||||
|
ViewerContentPreference(
|
||||||
|
countryCode = "KR",
|
||||||
|
isAdultContentVisible = true,
|
||||||
|
contentType = kr.co.vividnext.sodalive.content.ContentType.ALL,
|
||||||
|
isAdult = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.useCanCoupon(couponNumber = "COUPON5678", memberId = 2L)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("can.coupon.auth_required", exception.messageKey)
|
||||||
|
verify(issueService, never()).validateAvailableUseCoupon("COUPON5678", 2L)
|
||||||
|
verify(chargeService, never()).chargeByCoupon("COUPON5678", member)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(memberId: Long): Member {
|
||||||
|
return Member(
|
||||||
|
email = "member$memberId@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "member$memberId"
|
||||||
|
).apply {
|
||||||
|
id = memberId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user