Commit dd9870f0 authored by Rafał Tułaza's avatar Rafał Tułaza
Browse files

Msdkandrd-489 TDQ AUTH retry bugfix

parent 4e937583
......@@ -48,6 +48,9 @@ class PayByCardForm3DSViewModel @Inject constructor(
private val paymentTransactionManager =
PaymentTransactionManager(app, TrustPaymentsGatewayType.EU, false, BuildConfig.MERCHANT_USERNAME)
//please note that this session is created on page init intentionally
private val paymentSession = paymentTransactionManager.createSession({buildToken()})
private fun buildToken(): String {
val claimPayload =
StandardTPPayload(
......@@ -78,8 +81,6 @@ class PayByCardForm3DSViewModel @Inject constructor(
}
private suspend fun trigger3DSAndAuthRequest() {
val paymentSession = paymentTransactionManager.createSession({buildToken()})
paymentSession.cardPan = PAN
paymentSession.cardExpiryDate = expiryDate
paymentSession.cardSecurityCode = CVV
......
package com.trustpayments.mobile.core.eu
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.rule.ActivityTestRule
import com.trustpayments.mobile.core.BuildConfig
import com.trustpayments.mobile.core.R
import com.trustpayments.mobile.core.models.api.response.RequestType
import com.trustpayments.mobile.core.models.api.response.ResponseErrorCode
import com.trustpayments.mobile.core.services.transaction.ActivityResultProvider
import com.trustpayments.mobile.core.services.transaction.Error
import com.trustpayments.mobile.core.services.transaction.PaymentTransactionManager
import com.trustpayments.mobile.core.testutils.*
import com.trustpayments.mobile.core.ui.ThreeDSecureWebActivityResult
import com.trustpayments.mobile.core.util.JWTBuilder
import com.trustpayments.mobile.utils.core.testcardsdata.CardsDataWith3DSecureV2
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class RetryTestCases: BaseTestCase() {
private lateinit var paymentTransactionManager: PaymentTransactionManager
private lateinit var activity: ThreeDQueryTestActivity
private lateinit var activityResultProvider: ActivityResultProvider
@get:Rule
val initialActivityTestRule = ActivityTestRule(ThreeDQueryTestActivity::class.java)
private val jwtBuilder = JWTBuilder(BuildConfig.MERCHANT_USERNAME, BuildConfig.SITE_REFERENCE, BuildConfig.JWT_KEY)
@Before
override fun setUp() {
super.setUp()
val app = ApplicationProvider.getApplicationContext<Context>().applicationContext
paymentTransactionManager =
PaymentTransactionManager(app, getGatewayType(), false,
BuildConfig.MERCHANT_USERNAME
)
activity = initialActivityTestRule.activity
activityResultProvider = ActivityResultProvider().apply { result = ThreeDSecureWebActivityResult(0, 0, null) }
}
// .threeDQuery, .auth
@Test
fun test_negative_threedquery_auth_failedThreedquery() = runBlocking {
//step 1 - original request
// GIVEN
val jwtToken = jwtBuilder.getFor3DSecure(listOf(
RequestType.ThreeDQuery,
RequestType.Auth
), 14492)
val session =
paymentTransactionManager.createSessionFor(jwtToken, "4000000000002008", "123")
// WHEN
lateinit var sessionResult: PaymentTransactionManager.Response
var job = launch(Dispatchers.Unconfined) {
sessionResult =
paymentTransactionManager.executeSession(session, { activity }, activityResultProvider)
}
handle3DSV2Popup(
R.id.codeEditTextField, R.id.submitAuthenticationButton,
CardsDataWith3DSecureV2.threeDSecureCode
)
job.join()
var actualThreeDQueryTransactionResponse = sessionResult.getThreeDQueryTransactionResponse()
var actualAuthTransactionResponse = sessionResult.getAuthTransactionResponse()
// THEN
Assert.assertEquals(
actualThreeDQueryTransactionResponse.errorCode,
ResponseErrorCode.Ok
)
Assert.assertNotEquals(
actualAuthTransactionResponse.errorCode,
ResponseErrorCode.Ok
)
Assert.assertTrue(session.intermediateJwt.isEmpty())
//step 2 - retried request
// WHEN
job = launch(Dispatchers.Unconfined) {
sessionResult =
paymentTransactionManager.executeSession(session, { activity }, activityResultProvider)
}
handle3DSV2Popup(
R.id.codeEditTextField, R.id.submitAuthenticationButton,
CardsDataWith3DSecureV2.threeDSecureCode
)
job.join()
actualThreeDQueryTransactionResponse = sessionResult.getThreeDQueryTransactionResponse()
actualAuthTransactionResponse = sessionResult.getAuthTransactionResponse()
// THEN
Assert.assertEquals(
actualThreeDQueryTransactionResponse.errorCode,
ResponseErrorCode.Ok
)
Assert.assertNotEquals(
actualAuthTransactionResponse.errorCode,
ResponseErrorCode.Ok
)
Assert.assertTrue(session.intermediateJwt.isEmpty())
}
// .threeDQuery, .auth
@Test
fun test_negative_threedquery_auth_failedCardinalProcessing() = runBlocking {
//step 1 - original request
// GIVEN
val jwtToken = jwtBuilder.getFor3DSecure(listOf(
RequestType.ThreeDQuery,
RequestType.Auth
), 14492)
val session =
paymentTransactionManager.createSessionFor(jwtToken, "5200000000001104", "123")
// WHEN
lateinit var sessionResult: PaymentTransactionManager.Response
var job = launch(Dispatchers.Unconfined) {
sessionResult =
paymentTransactionManager.executeSession(session, { activity }, activityResultProvider)
}
handle3DSV2Popup(
R.id.codeEditTextField, R.id.submitAuthenticationButton,
CardsDataWith3DSecureV2.threeDSecureCode
)
job.join()
var actualThreeDQueryTransactionResponse = sessionResult.getThreeDQueryTransactionResponse()
// THEN
Assert.assertEquals(
actualThreeDQueryTransactionResponse.errorCode,
ResponseErrorCode.Ok
)
Assert.assertTrue(
sessionResult.error is Error.ThreeDSFailure
)
Assert.assertTrue(session.intermediateJwt.isEmpty())
//step 2 - retried request
// WHEN
job = launch(Dispatchers.Unconfined) {
sessionResult =
paymentTransactionManager.executeSession(session, { activity }, activityResultProvider)
}
handle3DSV2Popup(
R.id.codeEditTextField, R.id.submitAuthenticationButton,
CardsDataWith3DSecureV2.threeDSecureCode
)
job.join()
actualThreeDQueryTransactionResponse = sessionResult.getThreeDQueryTransactionResponse()
// THEN
Assert.assertEquals(
actualThreeDQueryTransactionResponse.errorCode,
ResponseErrorCode.Ok
)
Assert.assertTrue(
sessionResult.error is Error.ThreeDSFailure
)
Assert.assertTrue(session.intermediateJwt.isEmpty())
}
}
\ No newline at end of file
......@@ -70,7 +70,7 @@ public class CardinalCommerce3DSV1TestCasesJava extends BaseTestCase {
jwtToken = jwtBuilder.getFor3DSecure(new ArrayList<RequestType>() {{
add(RequestType.ThreeDQuery);
add(RequestType.Auth);
}});
}}, null);
activity = (ThreeDQueryTestActivity) initialActivityTestRule.getActivity();
......
......@@ -72,7 +72,7 @@ public class CardinalCommerce3DSV2TestCasesJava extends BaseTestCase {
jwtToken = jwtBuilder.getFor3DSecure(new ArrayList<RequestType>() {{
add(RequestType.ThreeDQuery);
add(RequestType.Auth);
}});
}}, null);
activity = (ThreeDQueryTestActivity) initialActivityTestRule.getActivity();
......
......@@ -67,6 +67,16 @@ fun PaymentTransactionManager.Response.getRiskDecTransactionResponse(): com.trus
it.requestTypeDescription == RequestType.RiskDec
}
fun PaymentTransactionManager.Response.getThreeDQueryTransactionResponse(): com.trustpayments.mobile.core.models.api.response.TransactionResponse =
ResponseParser.parse(this.responseJwtList)!!.flatMap { r: JwtResponse -> r.responses }.first {
it.requestTypeDescription == RequestType.ThreeDQuery
}
fun PaymentTransactionManager.Response.getAuthTransactionResponse(): com.trustpayments.mobile.core.models.api.response.TransactionResponse =
ResponseParser.parse(this.responseJwtList)!!.flatMap { r: JwtResponse -> r.responses }.first {
it.requestTypeDescription == RequestType.Auth
}
fun PaymentTransactionManager.Response.areAllResponsesSuccessful(): Boolean {
val parsedResponse = ResponseParser.parse(this.responseJwtList)
?: return false
......
......@@ -16,6 +16,10 @@ class PaymentSession internal constructor(
@JvmSynthetic get
@JvmSynthetic set
internal lateinit var intermediateJwt: String
@JvmSynthetic get
@JvmSynthetic set
internal var cacheJwt: String? = null
@JvmSynthetic get
@JvmSynthetic set
......
......@@ -103,59 +103,83 @@ class PaymentTransactionManager(
activityProvider: (() -> Activity)? = null,
activityResultProvider: ActivityResultProvider? = null): Response {
session.intermediateJwt = session.jwtProvider()
var requests = try {
parsePayload(jwt = session.jwtProvider()).requestTypes
parsePayload(jwt = session.intermediateJwt).requestTypes
} catch (ex: Exception) {
return Response(error = Error.ParsingError("Failed to extract request types from given JWT"))
return finishExecution(
session,
Response(error = Error.ParsingError("Failed to extract request types from given JWT")))
}
if (requests.isEmpty()) {
return Response(error = Error.ParsingError("List of request types in given JWT is empty"))
return finishExecution(
session,
Response(error = Error.ParsingError("List of request types in given JWT is empty")))
}
if(requests.any { it == RequestType.ThreeDQuery }) {
if(activityProvider == null) {
return Response(error = Error.DeveloperError.ActivityProviderRequired)
return finishExecution(
session,
Response(error = Error.DeveloperError.ActivityProviderRequired))
}
if(activityResultProvider == null) {
return Response(error = Error.DeveloperError.ActivityResultProviderRequired)
return finishExecution(
session,
Response(error = Error.DeveloperError.ActivityResultProviderRequired))
}
}
//Create Cardinal session to be able to check safety warnings
threeDSecureManager.createSession()
checkForSafetyWarnings()?.run { return Response(error = this) }
checkForSafetyWarnings()?.run {
return finishExecution(
session,
Response(error = this))
}
//Initialize session only if there's a 3DQuery request on the list
if(requests.any { it == RequestType.ThreeDQuery }) {
//If initialization returned any errors, return them here
val result = initializeSession(session)
if(result != null) {
return Response(error = result)
return finishExecution(
session,
Response(error = result))
}
//Deserialize request types again using JWT returned for JSINIT
requests = try {
parsePayload(jwt = session.jwtProvider()).requestTypes
parsePayload(jwt = session.intermediateJwt).requestTypes
} catch (ex: Exception) {
return Response(error = Error.ParsingError("Failed to extract request types from returned JWT"))
return finishExecution(
session,
Response(error = Error.ParsingError("Failed to extract request types from returned JWT")))
}
if (requests.isEmpty()) {
return Response(error = Error.ParsingError("List of request types in returned JWT is empty"))
return finishExecution(
session,
Response(error = Error.ParsingError("List of request types in returned JWT is empty")))
}
}
val firstResponse = executeSessionPart(session)
if (firstResponse !is TrustPaymentsApiService.Result.Success) {
return Response(error = Error.HttpFailure)
return finishExecution(
session,
Response(error = Error.HttpFailure))
}
//Find response with customerOutput set
val customerOutputResponse = firstResponse.transactionResponses.find { it.customerOutput != null }
val threeDQueryResponse = when (customerOutputResponse?.customerOutput) {
CustomerOutput.ThreeDRedirect -> customerOutputResponse
else -> return Response(listOf(firstResponse.responseJwt.token))
else -> return finishExecution(
session,
Response(listOf(firstResponse.responseJwt.token)))
}
//Parse payload of first response to later check if second request will be needed
......@@ -165,7 +189,9 @@ class PaymentTransactionManager(
payload.validate()
payload
} catch (ex: Exception) {
return Response(listOf(firstResponse.responseJwt.token), error = Error.ParsingError("Failed to parse server response"))
return finishExecution(
session,
Response(listOf(firstResponse.responseJwt.token), error = Error.ParsingError("Failed to parse server response")))
}
//Challenge requested, execute 3DS session
......@@ -181,19 +207,25 @@ class PaymentTransactionManager(
val threeDResponse = when (cardinalSessionResponse) {
is ThreeDSecureManager.ExecutionResult.SuccessV1 -> ThreeDResponse(pares = cardinalSessionResponse.result)
is ThreeDSecureManager.ExecutionResult.SuccessV2 -> ThreeDResponse(threeDResponse = cardinalSessionResponse.result)
is ThreeDSecureManager.ExecutionResult.Failure -> return Response(listOf(firstResponse.responseJwt.token), error = Error.ThreeDSFailure(cardinalSessionResponse.error))
is ThreeDSecureManager.ExecutionResult.Failure -> {
return finishExecution(
session,
Response(listOf(firstResponse.responseJwt.token), error = Error.ThreeDSFailure(cardinalSessionResponse.error)))
}
}
//If 3DS challenge completed successfully and there are no more requests to process, finish here
if(firstResponsePayload.requestTypes.isEmpty()) {
//No more requests to perform
return Response(listOf(firstResponse.responseJwt.token), threeDResponse)
return finishExecution(
session,
Response(listOf(firstResponse.responseJwt.token), threeDResponse))
}
//Execute remaining requests
//Update JWT
session.jwtProvider = { firstResponse.newJwt.token }
session.intermediateJwt = firstResponse.newJwt.token
val secondResponse = when (cardinalSessionResponse) {
is ThreeDSecureManager.ExecutionResult.SuccessV1 -> {
......@@ -208,10 +240,19 @@ class PaymentTransactionManager(
}
if (secondResponse !is TrustPaymentsApiService.Result.Success) {
return Response(listOf(firstResponse.responseJwt.token), threeDResponse, Error.HttpFailure)
return finishExecution(
session,
Response(listOf(firstResponse.responseJwt.token), threeDResponse, Error.HttpFailure))
}
return Response(listOf(firstResponse.responseJwt.token, secondResponse.responseJwt.token), threeDResponse)
return finishExecution(
session,
Response(listOf(firstResponse.responseJwt.token, secondResponse.responseJwt.token), threeDResponse))
}
private fun finishExecution(session: PaymentSession, response: Response): Response {
session.intermediateJwt = ""
return response
}
private suspend fun executeSessionPart(
......@@ -219,14 +260,14 @@ class PaymentTransactionManager(
threeDSecureJwt: String? = null, pares: String? = null): TrustPaymentsApiService.Result {
return apiService.performTransaction(
session.sessionId, merchantUsername, session.jwtProvider(), session.cacheJwt,
session.sessionId, merchantUsername, session.intermediateJwt, session.cacheJwt,
session.cardPan, session.cardExpiryDate, session.cardSecurityCode,
threeDSecureJwt, pares)
}
private suspend fun initializeSession(session: PaymentSession): Error? {
val result = apiService.initializeSession(
session.sessionId, merchantUsername, session.jwtProvider())
session.sessionId, merchantUsername, session.intermediateJwt)
if(result is TrustPaymentsApiService.Result.Success && result.transactionResponses.size == 1) {
//if result seems correct
......@@ -235,7 +276,7 @@ class PaymentTransactionManager(
if(jsInitResponse.errorCode == ResponseErrorCode.Ok && jsInitResponse.threeDInit != null) {
//always replace JWT with new JWT after JSINIT request
session.jwtProvider = { result.newJwt.token }
session.intermediateJwt = result.newJwt.token
session.cacheJwt = jsInitResponse.cacheToken
//TODO what to do if user does not execute the session?
......
......@@ -345,7 +345,8 @@ class PaymentTransactionManagerTest {
verifyThreeDSecureManagerInitializationCalledOnce()
verifyThreeDSecureManagerExecutionNeverCalled()
assertEquals(newJwt, session.jwtProvider())
assertEquals(responseJwt, session.jwtProvider())
assertEquals("", session.intermediateJwt)
assertTrue(result.responseJwtList.isEmpty())
assertNull(result.threeDResponse)
......@@ -380,7 +381,8 @@ class PaymentTransactionManagerTest {
verifyThreeDSecureManagerInitializationCalledOnce()
verifyThreeDSecureManagerExecutionNeverCalled()
assertEquals(newJwt, session.jwtProvider())
assertEquals(responseJwt, session.jwtProvider())
assertEquals("", session.intermediateJwt)
assertTrue(result.responseJwtList.isEmpty())
assertNull(result.threeDResponse)
......@@ -621,7 +623,8 @@ class PaymentTransactionManagerTest {
verifyThreeDSecureManagerInitializationCalledOnce()
verifyThreeDSecureManagerExecutionCalledOnce()
assertEquals(newJwt, session.jwtProvider())
assertEquals(jwt, session.jwtProvider())
assertEquals("", session.intermediateJwt)
assertEquals(1, result.responseJwtList.size)
assertEquals(jwt, result.responseJwtList[0])
......@@ -667,7 +670,8 @@ class PaymentTransactionManagerTest {
verifyThreeDSecureManagerInitializationCalledOnce()
verifyThreeDSecureManagerExecutionCalledOnce()
assertEquals(newJwt, session.jwtProvider())
assertEquals(jwt, session.jwtProvider())
assertEquals("", session.intermediateJwt)
assertEquals(2, result.responseJwtList.size)
assertEquals(jwt, result.responseJwtList[0])
......@@ -715,7 +719,8 @@ class PaymentTransactionManagerTest {
verifyThreeDSecureManagerInitializationCalledOnce()
verifyThreeDSecureManagerExecutionCalledOnce()
assertEquals(newJwt, session.jwtProvider())
assertEquals(jwt, session.jwtProvider())
assertEquals("", session.intermediateJwt)
assertEquals(2, result.responseJwtList.size)
assertEquals(jwt, result.responseJwtList[0])
......
......@@ -84,13 +84,13 @@ class JWTBuilder(
)
)
fun getFor3DSecure(requestTypes: List<RequestType>) =
fun getFor3DSecure(requestTypes: List<RequestType>, baseAmount: Int? = null) =
buildJWT(
merchantUsername,
StandardTPPayload(
siteReference,
"GBP",
1050,
baseAmount ?: 1050,
requestTypes.map { it.serializedName }
)
)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment