Test #466
@@ -50,7 +50,7 @@ class ChargeController(
|
||||
|
||||
val response = service.payverseVerify(memberId = member.id!!, verifyRequest)
|
||||
trackingCharge(member, response)
|
||||
ApiResponse.ok(Unit)
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
// Payverse Webhook 엔드포인트 (payverseVerify 아래)
|
||||
@@ -100,7 +100,7 @@ class ChargeController(
|
||||
|
||||
val response = service.verify(memberId = member.id!!, verifyRequest)
|
||||
trackingCharge(member, response)
|
||||
ApiResponse.ok(Unit)
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
@PostMapping("/verify/hecto")
|
||||
@@ -114,7 +114,7 @@ class ChargeController(
|
||||
|
||||
val response = service.verifyHecto(memberId = member.id!!, verifyRequest)
|
||||
trackingCharge(member, response)
|
||||
ApiResponse.ok(Unit)
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
@PostMapping("/apple")
|
||||
@@ -140,7 +140,7 @@ class ChargeController(
|
||||
|
||||
val response = service.appleVerify(memberId = member.id!!, verifyRequest)
|
||||
trackingCharge(member, response)
|
||||
ApiResponse.ok(Unit)
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
@PostMapping("/google")
|
||||
@@ -172,7 +172,7 @@ class ChargeController(
|
||||
)
|
||||
|
||||
trackingCharge(member, response)
|
||||
ApiResponse.ok(Unit)
|
||||
ApiResponse.ok(response)
|
||||
} else {
|
||||
throw SodaException(messageKey = "can.charge.invalid_payment_info")
|
||||
}
|
||||
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
package kr.co.vividnext.sodalive.can.charge
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.bootpay.Bootpay
|
||||
import kr.co.vividnext.sodalive.can.Can
|
||||
import kr.co.vividnext.sodalive.can.CanRepository
|
||||
import kr.co.vividnext.sodalive.can.CanStatus
|
||||
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.google.GooglePlayService
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobWorker
|
||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingService
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobRepository
|
||||
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobWorker
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import org.mockito.ArgumentCaptor
|
||||
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.MockBean
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:charge-controller-http;" +
|
||||
"MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
"jwt.secret=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZg==",
|
||||
"apple.iap-verify-url=https://apple.invalid/verifyReceipt",
|
||||
"apple.iap-verify-sandbox-url=https://apple-sandbox.invalid/verifyReceipt",
|
||||
"payverse.host=https://payverse.invalid",
|
||||
"payverse.inbound-ip=192.0.2.40",
|
||||
"payverse.usd-mid=http-usd-mid",
|
||||
"payverse.usd-client-key=http-usd-client",
|
||||
"payverse.usd-secret-key=http-usd-secret"
|
||||
]
|
||||
)
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
class ChargeControllerIntegrationTest @Autowired constructor(
|
||||
private val restTemplate: TestRestTemplate,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val chargeRepository: ChargeRepository,
|
||||
private val canRepository: CanRepository,
|
||||
private val memberRepository: MemberRepository,
|
||||
private val tokenRepository: MemberTokenRepository,
|
||||
private val tokenProvider: TokenProvider,
|
||||
private val jobRepository: ChargeEventJobRepository,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
private val entityManager: EntityManager
|
||||
) {
|
||||
@SpyBean
|
||||
private lateinit var service: ChargeService
|
||||
|
||||
@MockBean
|
||||
private lateinit var okHttpClient: OkHttpClient
|
||||
|
||||
@MockBean
|
||||
private lateinit var googlePlayService: GooglePlayService
|
||||
|
||||
@MockBean
|
||||
private lateinit var trackingService: AdTrackingService
|
||||
|
||||
@MockBean
|
||||
private lateinit var chargeEventJobWorker: ChargeEventJobWorker
|
||||
|
||||
@MockBean
|
||||
private lateinit var translationJobWorker: TranslationJobWorker
|
||||
|
||||
private lateinit var bootpay: Bootpay
|
||||
private val memberIds = mutableListOf<Long>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
val blockedRequest = Request.Builder().url("https://network-blocked.invalid").build()
|
||||
Mockito.doThrow(AssertionError("External HTTP is blocked"))
|
||||
.`when`(okHttpClient).newCall(Mockito.any(Request::class.java) ?: blockedRequest)
|
||||
Mockito.doThrow(AssertionError("Real Google verification is blocked"))
|
||||
.`when`(googlePlayService).verifyAndConsumePurchase(Mockito.anyString(), Mockito.anyString())
|
||||
bootpay = Mockito.mock(Bootpay::class.java)
|
||||
Mockito.doThrow(AssertionError("Real Bootpay token requests are blocked")).`when`(bootpay).accessToken
|
||||
Mockito.doThrow(AssertionError("Real Bootpay receipt requests are blocked"))
|
||||
.`when`(bootpay).getReceipt(Mockito.anyString())
|
||||
Mockito.doReturn(bootpay).`when`(service).createBootpay(Mockito.anyString(), Mockito.anyString())
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
tokenRepository.deleteAllById(memberIds)
|
||||
jobRepository.deleteAllInBatch()
|
||||
chargeRepository.deleteAll()
|
||||
canRepository.deleteAllInBatch()
|
||||
memberRepository.deleteAllById(memberIds)
|
||||
memberIds.clear()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(VerificationPath::class)
|
||||
@DisplayName("다섯 완료 HTTP 경로는 실제 서비스 결과의 네 필드와 기존 envelope를 반환한다")
|
||||
fun shouldReturnChargeCompleteDataForAllFiveVerificationEndpoints(path: VerificationPath) {
|
||||
val owner = createMember(container = if (path.isIap) "web" else "ios")
|
||||
val chargeId = if (path == VerificationPath.GOOGLE) {
|
||||
allowGooglePurchase()
|
||||
null
|
||||
} else {
|
||||
createCharge(owner, path.gateway, first = true)
|
||||
}
|
||||
val before = chargeId?.let { storedState(it, owner.id!!) }
|
||||
|
||||
val data = successData(post(path.path, verificationRequest(path, chargeId ?: 0), token(owner)))
|
||||
|
||||
assertCompletion(data, first = true, iap = path.isIap)
|
||||
if (chargeId == null) {
|
||||
val createdId = transactionTemplate.execute { chargeRepository.findAll().single().id!! }!!
|
||||
assertCompleted(createdId, owner.id!!, PaymentGateway.GOOGLE_IAP, listOf(0, 0, 100, 0, 0, 0))
|
||||
Mockito.verify(googlePlayService).verifyAndConsumePurchase("http-purchase", "http-product")
|
||||
Mockito.verifyNoInteractions(bootpay, okHttpClient)
|
||||
} else {
|
||||
assertEquals(before, storedState(chargeId, owner.id!!))
|
||||
assertNoProviderCalls()
|
||||
}
|
||||
assertTracking(owner.id!!, AdTrackingHistoryType.FIRST_PAYMENT)
|
||||
Mockito.verifyNoMoreInteractions(googlePlayService, trackingService)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = VerificationPath::class, names = ["GOOGLE"], mode = EnumSource.Mode.EXCLUDE)
|
||||
@DisplayName("저장된 true·false·null과 금액·통화는 재호출 및 단말 종류와 무관하게 직렬화된다")
|
||||
fun shouldSerializeCompletionFlagsAndPreservePriceAndCurrency(path: VerificationPath) {
|
||||
listOf(true to true, false to false, null to false).forEach { (first, expected) ->
|
||||
val owner = createMember(container = if (path.isIap) "web" else "aos")
|
||||
val chargeId = createCharge(owner, path.gateway, first = first)
|
||||
val before = storedState(chargeId, owner.id!!)
|
||||
val bearer = token(owner)
|
||||
|
||||
repeat(2) {
|
||||
assertCompletion(
|
||||
successData(post(path.path, verificationRequest(path, chargeId), bearer)),
|
||||
first = expected,
|
||||
iap = path.isIap
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(before, storedState(chargeId, owner.id!!))
|
||||
val type = if (first == true) AdTrackingHistoryType.FIRST_PAYMENT else AdTrackingHistoryType.REPEAT_PAYMENT
|
||||
assertTracking(owner.id!!, type, count = 2)
|
||||
}
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoMoreInteractions(trackingService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Google 신규 REQUEST는 기존 유료 이력이 있으면 false를 저장하고 REPEAT_PAYMENT를 전달한다")
|
||||
fun shouldCompleteNewGoogleChargeAsRepeatAfterLegacyPaidHistory() {
|
||||
val owner = createMember(container = "ios")
|
||||
val legacyId = createCharge(owner, PaymentGateway.GOOGLE_IAP, first = null)
|
||||
val legacyBefore = storedState(legacyId, owner.id!!)
|
||||
allowGooglePurchase()
|
||||
|
||||
assertCompletion(successData(post("/charge/google", googleRequest(), token(owner))), first = false, iap = true)
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val created = chargeRepository.findAll().single { it.id != legacyId }
|
||||
assertEquals(false, created.isFirstCharged)
|
||||
assertEquals(PaymentStatus.COMPLETE, created.payment!!.status)
|
||||
assertEquals("http-google-order", created.payment!!.orderId)
|
||||
}
|
||||
assertEquals(
|
||||
legacyBefore.copy(balances = listOf(listOf(0, 0, 100, 0, 0, 0)), chargeCount = 2),
|
||||
storedState(legacyId, owner.id!!)
|
||||
)
|
||||
assertTracking(owner.id!!, AdTrackingHistoryType.REPEAT_PAYMENT)
|
||||
Mockito.verify(googlePlayService).verifyAndConsumePurchase("http-purchase", "http-product")
|
||||
Mockito.verifyNoMoreInteractions(googlePlayService, trackingService)
|
||||
Mockito.verifyNoInteractions(bootpay, okHttpClient)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = VerificationPath::class, names = ["GOOGLE"], mode = EnumSource.Mode.EXCLUDE)
|
||||
@DisplayName("타인 소유 REQUEST·COMPLETE HTTP 요청은 동일한 기존 오류와 무변경 상태를 유지한다")
|
||||
fun shouldRejectForeignChargesBeforeShortcutOrMutation(path: VerificationPath) {
|
||||
listOf(PaymentStatus.REQUEST, PaymentStatus.COMPLETE).forEach { status ->
|
||||
val owner = createMember()
|
||||
val requester = createMember()
|
||||
val chargeId = createCharge(
|
||||
owner,
|
||||
path.gateway,
|
||||
status,
|
||||
first = if (status == PaymentStatus.COMPLETE) true else null
|
||||
)
|
||||
val before = storedState(chargeId, owner.id!!, requester.id!!)
|
||||
|
||||
val response = post(path.path, verificationRequest(path, chargeId), token(requester))
|
||||
|
||||
assertEquals(HttpStatus.OK, response.statusCode, response.body)
|
||||
val body = objectMapper.readTree(response.body)
|
||||
assertEnvelope(body)
|
||||
assertEquals(false, body["success"].booleanValue())
|
||||
assertEquals("There is an error with the payment information.", body["message"].textValue())
|
||||
assertEquals(true, body["data"].isNull)
|
||||
assertEquals(true, body["errorProperty"].isNull)
|
||||
assertEquals(before, storedState(chargeId, owner.id!!, requester.id!!))
|
||||
}
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoInteractions(trackingService)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(VerificationPath::class)
|
||||
@DisplayName("다섯 완료 HTTP 경로는 익명 요청을 401로 거부하고 charge를 생성하지 않는다")
|
||||
fun shouldRejectAnonymousVerificationBeforeCreation(path: VerificationPath) {
|
||||
val before = chargeRepository.count()
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, post(path.path, verificationRequest(path, 0)).statusCode)
|
||||
assertEquals(before, chargeRepository.count())
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoInteractions(trackingService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("partner PID와 미래 만료시각이 모두 유효한 회원에게만 기존 tracking이 전달된다")
|
||||
fun shouldKeepPartnerTrackingEligibility() {
|
||||
val future = LocalDateTime.now().plusDays(1)
|
||||
listOf(
|
||||
null to future,
|
||||
"" to future,
|
||||
" " to future,
|
||||
"http-partner" to null,
|
||||
"http-partner" to LocalDateTime.now().minusDays(1)
|
||||
).forEach { (pid, expiration) ->
|
||||
val owner = createMember(pid = pid, expiration = expiration)
|
||||
val chargeId = createCharge(owner, PaymentGateway.PG, first = true)
|
||||
assertCompletion(
|
||||
successData(post("/charge/verify", VerifyRequest("http-receipt", "$chargeId"), token(owner))),
|
||||
first = true,
|
||||
iap = false
|
||||
)
|
||||
}
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoInteractions(trackingService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PG·Apple·Payverse 생성 HTTP 응답은 chargeId 및 기존 payloadJson shape만 반환한다")
|
||||
fun shouldKeepCreationResponseShapes() {
|
||||
val owner = createMember()
|
||||
val bearer = token(owner)
|
||||
val can = canRepository.saveAndFlush(Can("HTTP pack", 100, 10, BigDecimal("9.99"), "USD", CanStatus.SALE))
|
||||
val requests = listOf(
|
||||
Triple("/charge", ChargeRequest(can.id!!, PaymentGateway.PG), PaymentGateway.PG),
|
||||
Triple(
|
||||
"/charge/apple",
|
||||
AppleChargeRequest("HTTP pack", 100, PaymentGateway.APPLE_IAP, BigDecimal("9.99"), "USD"),
|
||||
PaymentGateway.APPLE_IAP
|
||||
),
|
||||
Triple("/charge/payverse", PayverseChargeRequest(can.id!!), PaymentGateway.PAYVERSE)
|
||||
)
|
||||
requests.forEach { (path, request, gateway) ->
|
||||
val data = successData(post(path, request, bearer))
|
||||
val keys = if (path == "/charge/payverse") setOf("chargeId", "payloadJson") else setOf("chargeId")
|
||||
assertEquals(keys, data.fieldNames().asSequence().toSet())
|
||||
assertEquals(true, data["chargeId"].isIntegralNumber)
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val charge = chargeRepository.findById(data["chargeId"].longValue()).orElseThrow()
|
||||
assertEquals(owner.id, charge.member!!.id)
|
||||
assertEquals(PaymentStatus.REQUEST, charge.payment!!.status)
|
||||
assertEquals(gateway, charge.payment!!.paymentGateway)
|
||||
assertEquals(null, charge.isFirstCharged)
|
||||
}
|
||||
if (path == "/charge/payverse") {
|
||||
assertEquals(true, data["payloadJson"].isTextual)
|
||||
val payload = objectMapper.readTree(data["payloadJson"].textValue())
|
||||
assertEquals(
|
||||
setOf(
|
||||
"mid", "clientKey", "orderId", "customerId", "productName", "requestCurrency",
|
||||
"requestAmount", "reqDate", "sign"
|
||||
),
|
||||
payload.fieldNames().asSequence().toSet()
|
||||
)
|
||||
assertEquals(data["chargeId"].asText(), payload["orderId"].textValue())
|
||||
assertEquals("USD", payload["requestCurrency"].textValue())
|
||||
assertEquals(0, BigDecimal("9.99").compareTo(payload["requestAmount"].decimalValue()))
|
||||
}
|
||||
}
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoInteractions(trackingService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Payverse signed webhook은 기존 단일 key 응답과 IP·서명 검사 및 완료 replay를 유지한다")
|
||||
fun shouldKeepSignedWebhookResponseShapeAndRejectInvalidIpOrSignature() {
|
||||
val owner = createMember()
|
||||
val chargeId = createCharge(owner, PaymentGateway.PAYVERSE, PaymentStatus.REQUEST)
|
||||
val request = PayverseWebhookRequest(
|
||||
"PAYMENT", "http-usd-mid", "http-tid", "PVKR", "041", "$chargeId",
|
||||
"USD", BigDecimal("9.99"), "SUCCESS", "20261103120000",
|
||||
DigestUtils.sha512Hex("||http-usd-secret||http-usd-mid||$chargeId||9.99||20261103120000||")
|
||||
)
|
||||
val before = storedState(chargeId, owner.id!!)
|
||||
assertEquals(HttpStatus.NOT_FOUND, post("/charge/payverse/webhook", request, ip = "192.0.2.41").statusCode)
|
||||
assertEquals(before, storedState(chargeId, owner.id!!))
|
||||
assertEquals(
|
||||
HttpStatus.NOT_FOUND,
|
||||
post("/charge/payverse/webhook", request.copy(sign = "invalid"), ip = "192.0.2.40").statusCode
|
||||
)
|
||||
assertEquals(before, storedState(chargeId, owner.id!!))
|
||||
|
||||
repeat(2) {
|
||||
val response = post("/charge/payverse/webhook", request, ip = "192.0.2.40")
|
||||
assertEquals(HttpStatus.OK, response.statusCode, response.body)
|
||||
val body = objectMapper.readTree(response.body)
|
||||
assertEquals(setOf("receiveResult"), body.fieldNames().asSequence().toSet())
|
||||
assertEquals("SUCCESS", body["receiveResult"].textValue())
|
||||
assertCompleted(chargeId, owner.id!!, PaymentGateway.PAYVERSE, listOf(100, 10, 0, 0, 0, 0))
|
||||
}
|
||||
assertEquals(1L, chargeRepository.count())
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoInteractions(trackingService)
|
||||
val completed = storedState(chargeId, owner.id!!)
|
||||
assertCompletion(
|
||||
successData(post("/charge/payverse/verify", PayverseVerifyRequest("http-tid", "$chargeId"), token(owner))),
|
||||
first = true,
|
||||
iap = false
|
||||
)
|
||||
assertEquals(completed, storedState(chargeId, owner.id!!))
|
||||
assertTracking(owner.id!!, AdTrackingHistoryType.FIRST_PAYMENT)
|
||||
assertNoProviderCalls()
|
||||
Mockito.verifyNoMoreInteractions(trackingService)
|
||||
}
|
||||
|
||||
private fun createMember(
|
||||
pid: String? = "http-partner",
|
||||
expiration: LocalDateTime? = LocalDateTime.now().plusDays(1),
|
||||
container: String = "web"
|
||||
): Member {
|
||||
return memberRepository.saveAndFlush(
|
||||
Member(
|
||||
email = "http-${UUID.randomUUID()}@test.invalid",
|
||||
password = "test-password",
|
||||
nickname = "http-owner",
|
||||
activePid = pid,
|
||||
partnerExpirationDatetime = expiration,
|
||||
container = container
|
||||
)
|
||||
).also { memberIds.add(it.id!!) }
|
||||
}
|
||||
|
||||
private fun createCharge(
|
||||
owner: Member,
|
||||
gateway: PaymentGateway,
|
||||
status: PaymentStatus = PaymentStatus.COMPLETE,
|
||||
first: Boolean? = null
|
||||
): Long = transactionTemplate.execute {
|
||||
val charge = Charge(100, 10).apply {
|
||||
member = entityManager.getReference(Member::class.java, owner.id!!)
|
||||
isFirstCharged = first
|
||||
payment = Payment(status = status, paymentGateway = gateway).apply {
|
||||
price = BigDecimal("9.99")
|
||||
locale = "USD"
|
||||
receiptId = "stored-receipt"
|
||||
method = "stored-method"
|
||||
orderId = "stored-order"
|
||||
}
|
||||
}
|
||||
chargeRepository.saveAndFlush(charge).id!!
|
||||
}!!
|
||||
|
||||
private fun token(member: Member): String = tokenProvider.createToken(
|
||||
UsernamePasswordAuthenticationToken(member.email, "", listOf(SimpleGrantedAuthority("ROLE_USER"))),
|
||||
member.id!!
|
||||
)
|
||||
|
||||
private fun googleRequest() = GoogleChargeRequest(
|
||||
"HTTP pack",
|
||||
100,
|
||||
BigDecimal("9.99"),
|
||||
"USD",
|
||||
"http-product",
|
||||
"http-purchase",
|
||||
PaymentGateway.GOOGLE_IAP
|
||||
)
|
||||
|
||||
private fun verificationRequest(path: VerificationPath, chargeId: Long): Any = when (path) {
|
||||
VerificationPath.VERIFY, VerificationPath.HECTO -> VerifyRequest("http-receipt", "$chargeId")
|
||||
VerificationPath.PAYVERSE -> PayverseVerifyRequest("http-transaction", "$chargeId")
|
||||
VerificationPath.APPLE -> AppleVerifyRequest("http-receipt", chargeId)
|
||||
VerificationPath.GOOGLE -> googleRequest()
|
||||
}
|
||||
|
||||
private fun allowGooglePurchase() {
|
||||
Mockito.doReturn("http-google-order").`when`(googlePlayService)
|
||||
.verifyAndConsumePurchase("http-purchase", "http-product")
|
||||
}
|
||||
|
||||
private fun post(path: String, request: Any, bearer: String? = null, ip: String? = null): ResponseEntity<String> {
|
||||
val headers = HttpHeaders().apply {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
set(HttpHeaders.ACCEPT_LANGUAGE, "en")
|
||||
bearer?.let { setBearerAuth(it) }
|
||||
ip?.let { set("X-Forwarded-For", it) }
|
||||
}
|
||||
val response = restTemplate.exchange(path, HttpMethod.POST, HttpEntity(request, headers), String::class.java)
|
||||
println("HTTP EVIDENCE POST $path ${response.statusCodeValue}: ${response.body}")
|
||||
return response
|
||||
}
|
||||
|
||||
private fun assertEnvelope(body: JsonNode) {
|
||||
assertEquals(setOf("success", "message", "data", "errorProperty"), body.fieldNames().asSequence().toSet())
|
||||
assertEquals(true, body["success"].isBoolean)
|
||||
}
|
||||
|
||||
private fun successData(response: ResponseEntity<String>): JsonNode {
|
||||
assertEquals(HttpStatus.OK, response.statusCode, response.body)
|
||||
val body = objectMapper.readTree(response.body)
|
||||
assertEnvelope(body)
|
||||
assertEquals(true, body["success"].booleanValue(), response.body)
|
||||
assertEquals(true, body["message"].isNull)
|
||||
assertEquals(true, body["errorProperty"].isNull)
|
||||
assertEquals(true, body["data"].isObject, response.body)
|
||||
return body["data"]
|
||||
}
|
||||
|
||||
private fun assertCompletion(data: JsonNode, first: Boolean, iap: Boolean) {
|
||||
assertEquals(setOf("price", "currencyCode", "isFirstCharged", "isIap"), data.fieldNames().asSequence().toSet())
|
||||
assertEquals(true, data["price"].isNumber)
|
||||
assertEquals(0, BigDecimal("9.99").compareTo(data["price"].decimalValue()))
|
||||
assertEquals("USD", data["currencyCode"].textValue())
|
||||
listOf("isFirstCharged" to first, "isIap" to iap).forEach { (key, expected) ->
|
||||
assertEquals(true, data[key].isBoolean, data.toString())
|
||||
assertEquals(expected, data[key].booleanValue())
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertTracking(memberId: Long, type: AdTrackingHistoryType, count: Int = 1) {
|
||||
val prices = ArgumentCaptor.forClass(BigDecimal::class.java)
|
||||
Mockito.verify(trackingService, Mockito.times(count)).saveTrackingHistory(
|
||||
Mockito.eq("http-partner") ?: "",
|
||||
Mockito.eq(type) ?: type,
|
||||
Mockito.eq(memberId),
|
||||
prices.capture(),
|
||||
Mockito.eq("USD")
|
||||
)
|
||||
prices.allValues.forEach { assertEquals(0, BigDecimal("9.99").compareTo(it)) }
|
||||
}
|
||||
|
||||
private fun assertNoProviderCalls() {
|
||||
Mockito.verifyNoInteractions(bootpay, okHttpClient, googlePlayService)
|
||||
Mockito.verify(service, Mockito.never()).createBootpay(Mockito.anyString(), Mockito.anyString())
|
||||
}
|
||||
|
||||
private fun assertCompleted(chargeId: Long, ownerId: Long, gateway: PaymentGateway, expectedBalances: List<Int>) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val charge = chargeRepository.findById(chargeId).orElseThrow()
|
||||
assertEquals(ownerId, charge.member!!.id)
|
||||
assertEquals(true, charge.isFirstCharged)
|
||||
assertEquals(PaymentStatus.COMPLETE, charge.payment!!.status)
|
||||
assertEquals(gateway, charge.payment!!.paymentGateway)
|
||||
assertEquals(expectedBalances, balances(memberRepository.findById(ownerId).orElseThrow()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun storedState(chargeId: Long, vararg memberIds: Long): StoredState = transactionTemplate.execute {
|
||||
val charge = chargeRepository.findById(chargeId).orElseThrow()
|
||||
val payment = charge.payment!!
|
||||
StoredState(
|
||||
charge.status,
|
||||
charge.isFirstCharged,
|
||||
charge.chargeCan,
|
||||
charge.rewardCan,
|
||||
payment.status,
|
||||
payment.paymentGateway,
|
||||
payment.price,
|
||||
payment.locale,
|
||||
payment.receiptId,
|
||||
payment.method,
|
||||
payment.orderId,
|
||||
memberIds.map { balances(memberRepository.findById(it).orElseThrow()) },
|
||||
chargeRepository.count(),
|
||||
jobRepository.count()
|
||||
)
|
||||
}!!
|
||||
|
||||
private fun balances(member: Member) = listOf(
|
||||
member.pgChargeCan,
|
||||
member.pgRewardCan,
|
||||
member.googleChargeCan,
|
||||
member.googleRewardCan,
|
||||
member.appleChargeCan,
|
||||
member.appleRewardCan
|
||||
)
|
||||
|
||||
enum class VerificationPath(val path: String, val gateway: PaymentGateway, val isIap: Boolean) {
|
||||
VERIFY("/charge/verify", PaymentGateway.PG, false),
|
||||
HECTO("/charge/verify/hecto", PaymentGateway.PG, false),
|
||||
PAYVERSE("/charge/payverse/verify", PaymentGateway.PAYVERSE, false),
|
||||
APPLE("/charge/apple/verify", PaymentGateway.APPLE_IAP, true),
|
||||
GOOGLE("/charge/google", PaymentGateway.GOOGLE_IAP, true)
|
||||
}
|
||||
|
||||
private data class StoredState(
|
||||
val chargeStatus: ChargeStatus,
|
||||
val first: Boolean?,
|
||||
val chargeCan: Int,
|
||||
val rewardCan: Int,
|
||||
val paymentStatus: PaymentStatus,
|
||||
val gateway: PaymentGateway,
|
||||
val price: BigDecimal,
|
||||
val locale: String?,
|
||||
val receiptId: String?,
|
||||
val method: String?,
|
||||
val orderId: String?,
|
||||
val balances: List<List<Int>>,
|
||||
val chargeCount: Long,
|
||||
val eventJobCount: Long
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user