From f2d00f2996ece141347e06116d110e3e50ec52db Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Wed, 18 Sep 2019 22:27:34 -0400 Subject: [PATCH] Fix logic for entering 3DS2 challenge flow `Stripe3ds2AuthResutl.Ares.transStatus` should be used to determine whether the 3DS2 challenge flow should be entered. ANDROID-418 --- .../com/stripe/android/PaymentController.kt | 4 +- .../android/model/Stripe3ds2AuthResult.kt | 40 ++- .../model/Stripe3ds2AuthResultFixtures.kt | 3 +- .../model/Stripe3ds2AuthResultTest.java | 314 ----------------- .../android/model/Stripe3ds2AuthResultTest.kt | 326 ++++++++++++++++++ 5 files changed, 355 insertions(+), 332 deletions(-) delete mode 100644 stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.java create mode 100644 stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.kt diff --git a/stripe/src/main/java/com/stripe/android/PaymentController.kt b/stripe/src/main/java/com/stripe/android/PaymentController.kt index 46777d7766c..8315276415c 100644 --- a/stripe/src/main/java/com/stripe/android/PaymentController.kt +++ b/stripe/src/main/java/com/stripe/android/PaymentController.kt @@ -196,7 +196,7 @@ internal open class PaymentController @VisibleForTesting constructor( @VisibleForTesting fun getClientSecret(data: Intent): String { - return data.getStringExtra(StripeIntentResultExtras.CLIENT_SECRET) + return requireNotNull(data.getStringExtra(StripeIntentResultExtras.CLIENT_SECRET)) } /** @@ -438,7 +438,7 @@ internal open class PaymentController @VisibleForTesting constructor( override fun onSuccess(result: Stripe3ds2AuthResult) { val ares = result.ares if (ares != null) { - if (ares.shouldChallenge()) { + if (ares.isChallenge) { startChallengeFlow(ares) } else { startFrictionlessFlow() diff --git a/stripe/src/main/java/com/stripe/android/model/Stripe3ds2AuthResult.kt b/stripe/src/main/java/com/stripe/android/model/Stripe3ds2AuthResult.kt index 9c0d0fb2cdd..24bc7243edf 100644 --- a/stripe/src/main/java/com/stripe/android/model/Stripe3ds2AuthResult.kt +++ b/stripe/src/main/java/com/stripe/android/model/Stripe3ds2AuthResult.kt @@ -2,7 +2,6 @@ package com.stripe.android.model import com.stripe.android.ObjectBuilder import com.stripe.android.model.StripeJsonUtils.optString -import java.util.HashMap import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -91,9 +90,10 @@ internal data class Stripe3ds2AuthResult constructor( private val messageExtension: List?, private val messageType: String?, private val messageVersion: String?, - private val sdkTransId: String? + private val sdkTransId: String?, + private val transStatus: String? ) { - fun shouldChallenge() = VALUE_YES == acsChallengeMandated + val isChallenge = VALUE_CHALLENGE == transStatus internal class Builder : ObjectBuilder { private var threeDSServerTransId: String? = null @@ -107,6 +107,7 @@ internal data class Stripe3ds2AuthResult constructor( private var messageType: String? = null private var messageVersion: String? = null private var sdkTransId: String? = null + private var transStatus: String? = null fun setThreeDSServerTransId(threeDSServerTransId: String?): Builder { this.threeDSServerTransId = threeDSServerTransId @@ -163,11 +164,17 @@ internal data class Stripe3ds2AuthResult constructor( return this } + fun setTransStatus(transStatus: String?): Builder { + this.transStatus = transStatus + return this + } + override fun build(): Ares { - return Ares(threeDSServerTransId, acsChallengeMandated, - acsSignedContent, acsTransId, acsUrl, authenticationType, - cardholderInfo, messageExtension, messageType, messageVersion, - sdkTransId) + return Ares( + threeDSServerTransId, acsChallengeMandated, acsSignedContent, acsTransId, + acsUrl, authenticationType, cardholderInfo, messageExtension, messageType, + messageVersion, sdkTransId, transStatus + ) } } @@ -182,9 +189,10 @@ internal data class Stripe3ds2AuthResult constructor( private const val FIELD_MESSAGE_TYPE = "messageType" private const val FIELD_MESSAGE_VERSION = "messageVersion" private const val FIELD_SDK_TRANS_ID = "sdkTransID" + private const val FIELD_TRANS_STATUS = "transStatus" private const val FIELD_THREE_DS_SERVER_TRANS_ID = "threeDSServerTransID" - internal const val VALUE_YES = "Y" + internal const val VALUE_CHALLENGE = "C" @JvmStatic @Throws(JSONException::class) @@ -202,6 +210,7 @@ internal data class Stripe3ds2AuthResult constructor( .setMessageType(aresJson.getString(FIELD_MESSAGE_TYPE)) .setMessageVersion(aresJson.getString(FIELD_MESSAGE_VERSION)) .setSdkTransId(optString(aresJson, FIELD_SDK_TRANS_ID)) + .setTransStatus(optString(aresJson, FIELD_TRANS_STATUS)) .setMessageExtension(MessageExtension.fromJson( aresJson.optJSONArray(FIELD_MESSAGE_EXTENSION))) .build() @@ -277,14 +286,15 @@ internal data class Stripe3ds2AuthResult constructor( @Throws(JSONException::class) private fun fromJson(json: JSONObject): MessageExtension { - val data = HashMap() val dataJson = json.optJSONObject(FIELD_DATA) - if (dataJson != null) { - val keys = dataJson.keys() - while (keys.hasNext()) { - val key = keys.next() - data[key] = dataJson.getString(key) - } + val data = if (dataJson != null) { + val keys = dataJson.names() ?: JSONArray() + (0 until keys.length()) + .map { idx -> keys.getString(idx) } + .map { key -> mapOf(key to dataJson.getString(key)) } + .reduce { acc, map -> acc.plus(map) } + } else { + emptyMap() } return MessageExtension( diff --git a/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultFixtures.kt b/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultFixtures.kt index db645b78cd6..dbdd18dda53 100644 --- a/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultFixtures.kt +++ b/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultFixtures.kt @@ -9,10 +9,11 @@ internal object Stripe3ds2AuthResultFixtures { @JvmField val ARES_CHALLENGE_FLOW = Stripe3ds2AuthResult.Builder() .setAres(Stripe3ds2AuthResult.Ares.Builder() - .setAcsChallengeMandated(Stripe3ds2AuthResult.Ares.VALUE_YES) + .setAcsChallengeMandated("Y") .setAcsTransId(UUID.randomUUID().toString()) .setSdkTransId(UUID.randomUUID().toString()) .setThreeDSServerTransId(UUID.randomUUID().toString()) + .setTransStatus(Stripe3ds2AuthResult.Ares.VALUE_CHALLENGE) .setMessageVersion("2.1.0") .setMessageType("ARes") .build()) diff --git a/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.java b/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.java deleted file mode 100644 index 87d5fe5f3c5..00000000000 --- a/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.stripe.android.model; - -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -@RunWith(RobolectricTestRunner.class) -public class Stripe3ds2AuthResultTest { - private static final String AUTH_RESULT_JSON = "{\n" + - "\t\"id\": \"threeds2_1Ecwz3CRMbs6FrXfThtfogua\",\n" + - "\t\"object\": \"three_d_secure_2\",\n" + - "\t\"ares\": {\n" + - "\t\t\"acsChallengeMandated\": \"Y\",\n" + - "\t\t\"acsSignedContent\": \"eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa\",\n" + - "\t\t\"acsTransID\": \"dd23c757-211a-4c1b-add5-06a1450a642e\",\n" + - "\t\t\"acsURL\": null,\n" + - "\t\t\"authenticationType\": \"02\",\n" + - "\t\t\"cardholderInfo\": null,\n" + - "\t\t\"messageExtension\": null,\n" + - "\t\t\"messageType\": \"ARes\",\n" + - "\t\t\"messageVersion\": \"2.1.0\",\n" + - "\t\t\"sdkTransID\": \"20158862-9d9d-4d71-83d4-9e65554ed92c\",\n" + - "\t\t\"threeDSServerTransID\": \"e8ea0b42-0e74-42b2-92b4-1b27005f0596\"\n" + - "\t},\n" + - "\t\"created\": 1558541285,\n" + - "\t\"error\": null,\n" + - "\t\"livemode\": false,\n" + - "\t\"source\": \"src_1Ecwz1CRMbs6FrXfUwt98lxf\",\n" + - "\t\"state\": \"challenge_required\"\n" + - "}"; - - private static final String AUTH_RESULT_WITH_EXTENSIONS_JSON = "{\n" + - "\t\"id\": \"threeds2_1Ecwz3CRMbs6FrXfThtfogua\",\n" + - "\t\"object\": \"three_d_secure_2\",\n" + - "\t\"ares\": {\n" + - "\t\t\"acsChallengeMandated\": \"Y\",\n" + - "\t\t\"acsSignedContent\": \"eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa\",\n" + - "\t\t\"acsTransID\": \"dd23c757-211a-4c1b-add5-06a1450a642e\",\n" + - "\t\t\"acsURL\": null,\n" + - "\t\t\"authenticationType\": \"02\",\n" + - "\t\t\"cardholderInfo\": null,\n" + - "\t\t\"messageExtension\": [\n" + - "\t\t\t{\n" + - "\t\t\t\t\"name\":\"extension1\",\n" + - "\t\t\t\t\"id\":\"ID1\",\n" + - "\t\t\t\t\"criticalityIndicator\":true,\n" + - "\t\t\t\t\"data\":{\n" + - "\t\t\t\t\t\"valueOne\":\"value\"\n" + - "\t\t\t\t}\n" + - "\t\t\t},\n" + - "\t\t\t{\n" + - "\t\t\t\t\"name\":\"extension2\",\n" + - "\t\t\t\t\"id\":\"ID2\",\n" + - "\t\t\t\t\"criticalityIndicator\":true,\n" + - "\t\t\t\t\"data\":{\n" + - "\t\t\t\t\t\"valueOne\":\"value1\",\n" + - "\t\t\t\t\t\"valueTwo\":\"value2\"\n" + - "\t\t\t\t}\n" + - "\t\t\t},\n" + - "\t\t\t{\n" + - "\t\t\t\t\"name\":\"sharedData\",\n" + - "\t\t\t\t\"id\":\"ID3\",\n" + - "\t\t\t\t\"criticalityIndicator\":false,\n" + - "\t\t\t\t\"data\":{\n" + - "\t\t\t\t\t\"value3\":\"IkpTT05EYXRhIjogew0KImRhdGExIjogInNkYXRhIg0KfQ==\"\n" + - "\t\t\t\t}\n" + - "\t\t\t}\n" + - "\t\t],\n" + - "\t\t\"messageType\": \"ARes\",\n" + - "\t\t\"messageVersion\": \"2.1.0\",\n" + - "\t\t\"sdkTransID\": \"20158862-9d9d-4d71-83d4-9e65554ed92c\",\n" + - "\t\t\"threeDSServerTransID\": \"e8ea0b42-0e74-42b2-92b4-1b27005f0596\"\n" + - "\t},\n" + - "\t\"created\": 1558541285,\n" + - "\t\"error\": null,\n" + - "\t\"livemode\": false,\n" + - "\t\"source\": \"src_1Ecwz1CRMbs6FrXfUwt98lxf\",\n" + - "\t\"state\": \"challenge_required\"\n" + - "}"; - - private static final String AUTH_RESULT_ERROR_JSON = "{\n" + - "\t\"id\": \"threeds2_1Ecwz3CRMbs6FrXfThtfogua\",\n" + - "\t\"object\": \"three_d_secure_2\",\n" + - "\t\"ares\": {\n" + - "\t\t\"acsChallengeMandated\": \"Y\",\n" + - "\t\t\"acsSignedContent\": \"eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa\",\n" + - "\t\t\"acsTransID\": \"dd23c757-211a-4c1b-add5-06a1450a642e\",\n" + - "\t\t\"acsURL\": null,\n" + - "\t\t\"authenticationType\": \"02\",\n" + - "\t\t\"cardholderInfo\": null,\n" + - "\t\t\"messageExtension\": null,\n" + - "\t\t\"messageType\": \"ARes\",\n" + - "\t\t\"messageVersion\": \"2.1.0\",\n" + - "\t\t\"sdkTransID\": \"20158862-9d9d-4d71-83d4-9e65554ed92c\",\n" + - "\t\t\"threeDSServerTransID\": \"e8ea0b42-0e74-42b2-92b4-1b27005f0596\"\n" + - "\t},\n" + - "\t\"created\": 1558541285,\n" + - "\t\"error\": {\n" + - "\t\t\"threeDSServerTransID\": \"e8ea0b42-0e74-42b2-92b4-1b27005f0596\",\n" + - "\t\t\"acsTransID\": \"dd23c757-211a-4c1b-add5-06a1450a642e\",\n" + - "\t\t\"dsTransID\": \"ff23c757-211a-4c1b-add5-06a1450a642e\",\n" + - "\t\t\"errorCode\": \"error code 1234\",\n" + - "\t\t\"errorDescription\": \"error description\",\n" + - "\t\t\"errorDetail\": \"error detail\",\n" + - "\t\t\"errorComponent\": \"error component\",\n" + - "\t\t\"errorMessageType\": \"error message type\",\n" + - "\t\t\"messageType\": \"Error\",\n" + - "\t\t\"messageVersion\": \"2.1.0\",\n" + - "\t\t\"sdkTransID\": \"20158862-9d9d-4d71-83d4-9e65554ed92c\"\n" + - "\t},\n" + - "\t\"livemode\": false,\n" + - "\t\"source\": \"src_1Ecwz1CRMbs6FrXfUwt98lxf\",\n" + - "\t\"state\": \"challenge_required\"\n" + - "}"; - - private static final String AUTH_RESULT_ERROR_INVALID_ELEMENT_FORMAT_JSON = "{\n" + - "\t\"ares\": null,\n" + - "\t\"livemode\": true,\n" + - "\t\"created\": 1562711486,\n" + - "\t\"id\": \"threeds2_1EuRqMAWhjPjYwPi83sPpdVY\",\n" + - "\t\"source\": \"src_1EuRqGAWhjPjYwPid0T5ZrrF\",\n" + - "\t\"state\": \"failed\",\n" + - "\t\"error\": {\n" + - "\t\t\"errorComponent\": \"D\",\n" + - "\t\t\"acsTransID\": null,\n" + - "\t\t\"errorDescription\": \"Format or value of one or more Data Elements is Invalid according to the Specification\",\n" + - "\t\t\"messageType\": \"Erro\",\n" + - "\t\t\"dsTransID\": \"3fa2e398-4146-42f0-b905-a52c06b5caa2\",\n" + - "\t\t\"errorCode\": \"203\",\n" + - "\t\t\"errorDetail\": \"sdkMaxTimeout\",\n" + - "\t\t\"errorMessageType\": \"AReq\",\n" + - "\t\t\"messageVersion\": \"2.1.0\",\n" + - "\t\t\"sdkTransID\": \"a9e7db5d-e95c-4cc6-a8b7-df1cee092879\",\n" + - "\t\t\"threeDSServerTransID\": \"161d5143-340c-4e40-8ee1-a272be64aecc\"\n" + - "\t},\n" + - "\t\"object\": \"three_d_secure_2\"\n" + - "}"; - - private static final String AUTH_RESULT_FALLBACK_REDIRECT_URL_JSON = "{\n" + - "\t\"ares\": null,\n" + - "\t\"livemode\": true,\n" + - "\t\"created\": 1562711486,\n" + - "\t\"id\": \"threeds2_1EuRqMAWhjPjYwPi83sPpdVY\",\n" + - "\t\"source\": \"src_1EuRqGAWhjPjYwPid0T5ZrrF\",\n" + - "\t\"state\": \"failed\",\n" + - "\t\"fallback_redirect_url\": \"https://hooks.stripe.com/3d_secure_2_eap/begin_test/src_1Ecve7CRMbs6FrXfm8AxXMIh/src_client_secret_F79yszOBAiuaZTuIhbn3LPUW\",\n" + - "\t\"object\": \"three_d_secure_2\"\n" + - "}"; - - @Test - public void fromJSON_validData_createsObject() throws JSONException { - final Stripe3ds2AuthResult jsonResult = Stripe3ds2AuthResult - .fromJson(new JSONObject(AUTH_RESULT_JSON)); - - final Stripe3ds2AuthResult expectedResult = new Stripe3ds2AuthResult.Builder() - .setId("threeds2_1Ecwz3CRMbs6FrXfThtfogua") - .setObjectType("three_d_secure_2") - .setLiveMode(false) - .setCreated(1558541285L) - .setSource("src_1Ecwz1CRMbs6FrXfUwt98lxf") - .setState("challenge_required") - .setAres(new Stripe3ds2AuthResult.Ares.Builder() - .setAcsChallengeMandated("Y") - .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") - .setAcsSignedContent("eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa") - .setAuthenticationType("02") - .setMessageType("ARes") - .setMessageVersion("2.1.0") - .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") - .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") - .build()) - .build(); - - assertEquals(expectedResult, jsonResult); - } - - @Test - public void fromJSON_dataWithMessageExtensions_createsObject() throws JSONException { - final Stripe3ds2AuthResult jsonResult = Stripe3ds2AuthResult - .fromJson(new JSONObject(AUTH_RESULT_WITH_EXTENSIONS_JSON)); - - final AbstractMap extension1Data = new HashMap<>(1); - extension1Data.put("valueOne", "value"); - - final AbstractMap extension2Data = new HashMap<>(2); - extension2Data.put("valueOne", "value1"); - extension2Data.put("valueTwo", "value2"); - - final AbstractMap extension3Data = new HashMap<>(1); - extension3Data.put("value3", "IkpTT05EYXRhIjogew0KImRhdGExIjogInNkYXRhIg0KfQ=="); - - final List extensions = Arrays.asList( - new Stripe3ds2AuthResult.MessageExtension.Builder() - .setName("extension1") - .setId("ID1") - .setCriticalityIndicator(true) - .setData(extension1Data) - .build(), - new Stripe3ds2AuthResult.MessageExtension.Builder() - .setName("extension2") - .setId("ID2") - .setCriticalityIndicator(true) - .setData(extension2Data) - .build(), - new Stripe3ds2AuthResult.MessageExtension.Builder() - .setName("sharedData") - .setId("ID3") - .setCriticalityIndicator(false) - .setData(extension3Data) - .build() - ); - - final Stripe3ds2AuthResult expectedResult = new Stripe3ds2AuthResult.Builder() - .setId("threeds2_1Ecwz3CRMbs6FrXfThtfogua") - .setObjectType("three_d_secure_2") - .setLiveMode(false) - .setCreated(1558541285L) - .setSource("src_1Ecwz1CRMbs6FrXfUwt98lxf") - .setState("challenge_required") - .setAres(new Stripe3ds2AuthResult.Ares.Builder() - .setAcsChallengeMandated("Y") - .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") - .setAcsSignedContent("eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa") - .setAuthenticationType("02") - .setMessageType("ARes") - .setMessageVersion("2.1.0") - .setMessageExtension(extensions) - .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") - .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") - .build()) - .build(); - - assertEquals(expectedResult, jsonResult); - } - - @Test - public void fromJSON_errorData_createsObjectWithError() throws JSONException { - final Stripe3ds2AuthResult jsonResult = Stripe3ds2AuthResult - .fromJson(new JSONObject(AUTH_RESULT_ERROR_JSON)); - - final Stripe3ds2AuthResult expectedResult = new Stripe3ds2AuthResult.Builder() - .setId("threeds2_1Ecwz3CRMbs6FrXfThtfogua") - .setObjectType("three_d_secure_2") - .setLiveMode(false) - .setCreated(1558541285L) - .setSource("src_1Ecwz1CRMbs6FrXfUwt98lxf") - .setState("challenge_required") - .setAres(new Stripe3ds2AuthResult.Ares.Builder() - .setAcsChallengeMandated("Y") - .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") - .setAcsSignedContent("eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa") - .setAuthenticationType("02") - .setMessageType("ARes") - .setMessageVersion("2.1.0") - .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") - .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") - .build()) - .setError(new Stripe3ds2AuthResult.ThreeDS2Error.Builder() - .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") - .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") - .setDsTransId("ff23c757-211a-4c1b-add5-06a1450a642e") - .setErrorCode("error code 1234") - .setErrorComponent("error component") - .setErrorDetail("error detail") - .setErrorDescription("error description") - .setErrorMessageType("error message type") - .setMessageType("Error") - .setMessageVersion("2.1.0") - .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") - .build()) - .build(); - - assertEquals(expectedResult, jsonResult); - } - - @Test - public void fromJson_invalidElementFormatJson_shouldPopulateErrorField() throws JSONException { - final Stripe3ds2AuthResult result = Stripe3ds2AuthResult.fromJson( - new JSONObject(AUTH_RESULT_ERROR_INVALID_ELEMENT_FORMAT_JSON)); - assertNull(result.getAres()); - assertNull(result.getFallbackRedirectUrl()); - assertNotNull(result.getError()); - assertEquals( - "sdkMaxTimeout", - result.getError().getErrorDetail()); - assertEquals( - "Format or value of one or more Data Elements is Invalid according to the Specification", - result.getError().getErrorDescription()); - } - - @Test - public void fromJson_fallbackRedirectUrl_shouldReturnValidRedirectData() throws JSONException { - final Stripe3ds2AuthResult result = Stripe3ds2AuthResult.fromJson( - new JSONObject(AUTH_RESULT_FALLBACK_REDIRECT_URL_JSON)); - assertNull(result.getAres()); - assertNull(result.getError()); - - assertEquals( - "https://hooks.stripe.com/3d_secure_2_eap/begin_test/src_1Ecve7CRMbs6FrXfm8AxXMIh/src_client_secret_F79yszOBAiuaZTuIhbn3LPUW", - result.getFallbackRedirectUrl() - ); - } -} diff --git a/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.kt b/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.kt new file mode 100644 index 00000000000..3b0bea05d8a --- /dev/null +++ b/stripe/src/test/java/com/stripe/android/model/Stripe3ds2AuthResultTest.kt @@ -0,0 +1,326 @@ +package com.stripe.android.model + +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class Stripe3ds2AuthResultTest { + + @Test + @Throws(JSONException::class) + fun fromJSON_validData_createsObject() { + val result = Stripe3ds2AuthResult.fromJson(AUTH_RESULT_JSON) + assertTrue(result.ares?.isChallenge == true) + + val expectedResult = Stripe3ds2AuthResult.Builder() + .setId("threeds2_1Ecwz3CRMbs6FrXfThtfogua") + .setObjectType("three_d_secure_2") + .setLiveMode(false) + .setCreated(1558541285L) + .setSource("src_1Ecwz1CRMbs6FrXfUwt98lxf") + .setState("challenge_required") + .setAres(Stripe3ds2AuthResult.Ares.Builder() + .setAcsChallengeMandated("Y") + .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") + .setAcsSignedContent("eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa") + .setAuthenticationType("02") + .setMessageType("ARes") + .setMessageVersion("2.1.0") + .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") + .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") + .setTransStatus("C") + .build()) + .build() + + assertEquals(expectedResult, result) + } + + @Test + @Throws(JSONException::class) + fun fromJSON_dataWithMessageExtensions_createsObject() { + val jsonResult = Stripe3ds2AuthResult.fromJson(AUTH_RESULT_WITH_EXTENSIONS_JSON) + + val extensions = listOf( + Stripe3ds2AuthResult.MessageExtension.Builder() + .setName("extension1") + .setId("ID1") + .setCriticalityIndicator(true) + .setData(mapOf("key1" to "value1")) + .build(), + Stripe3ds2AuthResult.MessageExtension.Builder() + .setName("extension2") + .setId("ID2") + .setCriticalityIndicator(true) + .setData(mapOf( + "key1" to "value1", + "key2" to "value2" + )) + .build(), + Stripe3ds2AuthResult.MessageExtension.Builder() + .setName("sharedData") + .setId("ID3") + .setCriticalityIndicator(false) + .setData(mapOf("key" to "IkpTT05EYXRhIjogew0KImRhdGExIjogInNkYXRhIg0KfQ==")) + .build() + ) + + val expectedResult = Stripe3ds2AuthResult.Builder() + .setId("threeds2_1Ecwz3CRMbs6FrXfThtfogua") + .setObjectType("three_d_secure_2") + .setLiveMode(false) + .setCreated(1558541285L) + .setSource("src_1Ecwz1CRMbs6FrXfUwt98lxf") + .setState("challenge_required") + .setAres(Stripe3ds2AuthResult.Ares.Builder() + .setAcsChallengeMandated("Y") + .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") + .setAcsSignedContent("eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa") + .setAuthenticationType("02") + .setMessageType("ARes") + .setMessageVersion("2.1.0") + .setMessageExtension(extensions) + .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") + .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") + .build()) + .build() + + assertEquals(expectedResult, jsonResult) + } + + @Test + @Throws(JSONException::class) + fun fromJSON_errorData_createsObjectWithError() { + val jsonResult = Stripe3ds2AuthResult.fromJson(AUTH_RESULT_ERROR_JSON) + + val expectedResult = Stripe3ds2AuthResult.Builder() + .setId("threeds2_1Ecwz3CRMbs6FrXfThtfogua") + .setObjectType("three_d_secure_2") + .setLiveMode(false) + .setCreated(1558541285L) + .setSource("src_1Ecwz1CRMbs6FrXfUwt98lxf") + .setState("challenge_required") + .setAres(Stripe3ds2AuthResult.Ares.Builder() + .setAcsChallengeMandated("Y") + .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") + .setAcsSignedContent("eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa") + .setAuthenticationType("02") + .setMessageType("ARes") + .setMessageVersion("2.1.0") + .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") + .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") + .build()) + .setError(Stripe3ds2AuthResult.ThreeDS2Error.Builder() + .setThreeDSServerTransId("e8ea0b42-0e74-42b2-92b4-1b27005f0596") + .setAcsTransId("dd23c757-211a-4c1b-add5-06a1450a642e") + .setDsTransId("ff23c757-211a-4c1b-add5-06a1450a642e") + .setErrorCode("error code 1234") + .setErrorComponent("error component") + .setErrorDetail("error detail") + .setErrorDescription("error description") + .setErrorMessageType("error message type") + .setMessageType("Error") + .setMessageVersion("2.1.0") + .setSdkTransId("20158862-9d9d-4d71-83d4-9e65554ed92c") + .build()) + .build() + + assertEquals(expectedResult, jsonResult) + } + + @Test + @Throws(JSONException::class) + fun fromJson_invalidElementFormatJson_shouldPopulateErrorField() { + val result = Stripe3ds2AuthResult.fromJson(AUTH_RESULT_ERROR_INVALID_ELEMENT_FORMAT_JSON) + assertNull(result.ares) + assertNull(result.fallbackRedirectUrl) + val error = result.error + assertNotNull(error) + assertEquals("sdkMaxTimeout", error?.errorDetail) + assertEquals( + "Format or value of one or more Data Elements is Invalid according to the Specification", + error?.errorDescription) + } + + @Test + @Throws(JSONException::class) + fun fromJson_fallbackRedirectUrl_shouldReturnValidRedirectData() { + val result = Stripe3ds2AuthResult.fromJson(AUTH_RESULT_FALLBACK_REDIRECT_URL_JSON) + assertNull(result.ares) + assertNull(result.error) + + assertEquals( + "https://hooks.stripe.com/3d_secure_2_eap/begin_test/src_1Ecve7CRMbs6FrXfm8AxXMIh/src_client_secret_F79yszOBAiuaZTuIhbn3LPUW", + result.fallbackRedirectUrl + ) + } + + companion object { + private val AUTH_RESULT_JSON = JSONObject( + """ + { + "id": "threeds2_1Ecwz3CRMbs6FrXfThtfogua", + "object": "three_d_secure_2", + "ares": { + "acsChallengeMandated": "Y", + "acsSignedContent": "eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa", + "acsTransID": "dd23c757-211a-4c1b-add5-06a1450a642e", + "acsURL": null, + "authenticationType": "02", + "cardholderInfo": null, + "messageExtension": null, + "messageType": "ARes", + "messageVersion": "2.1.0", + "sdkTransID": "20158862-9d9d-4d71-83d4-9e65554ed92c", + "threeDSServerTransID": "e8ea0b42-0e74-42b2-92b4-1b27005f0596", + "transStatus": "C" + }, + "created": 1558541285, + "error": null, + "livemode": false, + "source": "src_1Ecwz1CRMbs6FrXfUwt98lxf", + "state": "challenge_required" + } + """.trimIndent() + ) + + private val AUTH_RESULT_WITH_EXTENSIONS_JSON = JSONObject( + """ + { + "id": "threeds2_1Ecwz3CRMbs6FrXfThtfogua", + "object": "three_d_secure_2", + "ares": { + "acsChallengeMandated": "Y", + "acsSignedContent": "eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa", + "acsTransID": "dd23c757-211a-4c1b-add5-06a1450a642e", + "acsURL": null, + "authenticationType": "02", + "cardholderInfo": null, + "messageExtension": [{ + "name": "extension1", + "id": "ID1", + "criticalityIndicator": true, + "data": { + "key1": "value1" + } + }, + { + "name": "extension2", + "id": "ID2", + "criticalityIndicator": true, + "data": { + "key1": "value1", + "key2": "value2" + } + }, + { + "name": "sharedData", + "id": "ID3", + "criticalityIndicator": false, + "data": { + "key": "IkpTT05EYXRhIjogew0KImRhdGExIjogInNkYXRhIg0KfQ==" + } + } + ], + "messageType": "ARes", + "messageVersion": "2.1.0", + "sdkTransID": "20158862-9d9d-4d71-83d4-9e65554ed92c", + "threeDSServerTransID": "e8ea0b42-0e74-42b2-92b4-1b27005f0596" + }, + "created": 1558541285, + "error": null, + "livemode": false, + "source": "src_1Ecwz1CRMbs6FrXfUwt98lxf", + "state": "challenge_required" + } + """.trimIndent() + ) + + private val AUTH_RESULT_ERROR_JSON = JSONObject( + """ + { + "id": "threeds2_1Ecwz3CRMbs6FrXfThtfogua", + "object": "three_d_secure_2", + "ares": { + "acsChallengeMandated": "Y", + "acsSignedContent": "eyJhbGciOiJFUzI1NiJ9.asdfasf.asdfasdfa", + "acsTransID": "dd23c757-211a-4c1b-add5-06a1450a642e", + "acsURL": null, + "authenticationType": "02", + "cardholderInfo": null, + "messageExtension": null, + "messageType": "ARes", + "messageVersion": "2.1.0", + "sdkTransID": "20158862-9d9d-4d71-83d4-9e65554ed92c", + "threeDSServerTransID": "e8ea0b42-0e74-42b2-92b4-1b27005f0596" + }, + "created": 1558541285, + "error": { + "threeDSServerTransID": "e8ea0b42-0e74-42b2-92b4-1b27005f0596", + "acsTransID": "dd23c757-211a-4c1b-add5-06a1450a642e", + "dsTransID": "ff23c757-211a-4c1b-add5-06a1450a642e", + "errorCode": "error code 1234", + "errorDescription": "error description", + "errorDetail": "error detail", + "errorComponent": "error component", + "errorMessageType": "error message type", + "messageType": "Error", + "messageVersion": "2.1.0", + "sdkTransID": "20158862-9d9d-4d71-83d4-9e65554ed92c" + }, + "livemode": false, + "source": "src_1Ecwz1CRMbs6FrXfUwt98lxf", + "state": "challenge_required" + } + """.trimIndent() + ) + + private val AUTH_RESULT_ERROR_INVALID_ELEMENT_FORMAT_JSON = JSONObject( + """ + { + "ares": null, + "livemode": true, + "created": 1562711486, + "id": "threeds2_1EuRqMAWhjPjYwPi83sPpdVY", + "source": "src_1EuRqGAWhjPjYwPid0T5ZrrF", + "state": "failed", + "error": { + "errorComponent": "D", + "acsTransID": null, + "errorDescription": "Format or value of one or more Data Elements is Invalid according to the Specification", + "messageType": "Erro", + "dsTransID": "3fa2e398-4146-42f0-b905-a52c06b5caa2", + "errorCode": "203", + "errorDetail": "sdkMaxTimeout", + "errorMessageType": "AReq", + "messageVersion": "2.1.0", + "sdkTransID": "a9e7db5d-e95c-4cc6-a8b7-df1cee092879", + "threeDSServerTransID": "161d5143-340c-4e40-8ee1-a272be64aecc" + }, + "object": "three_d_secure_2" + } + """.trimIndent() + ) + + private val AUTH_RESULT_FALLBACK_REDIRECT_URL_JSON = JSONObject( + """ + { + "ares": null, + "livemode": true, + "created": 1562711486, + "id": "threeds2_1EuRqMAWhjPjYwPi83sPpdVY", + "source": "src_1EuRqGAWhjPjYwPid0T5ZrrF", + "state": "failed", + "fallback_redirect_url": "https://hooks.stripe.com/3d_secure_2_eap/begin_test/src_1Ecve7CRMbs6FrXfm8AxXMIh/src_client_secret_F79yszOBAiuaZTuIhbn3LPUW", + "object": "three_d_secure_2" + } + """.trimIndent() + ) + } +}