diff --git a/CardPayments/src/main/java/com/paypal/android/cardpayments/CardClient.kt b/CardPayments/src/main/java/com/paypal/android/cardpayments/CardClient.kt index d034cf5a2..5e1fd2a82 100644 --- a/CardPayments/src/main/java/com/paypal/android/cardpayments/CardClient.kt +++ b/CardPayments/src/main/java/com/paypal/android/cardpayments/CardClient.kt @@ -8,7 +8,6 @@ import androidx.activity.ComponentActivity import com.paypal.android.cardpayments.api.CheckoutOrdersAPI import com.paypal.android.cardpayments.api.ConfirmPaymentSourceResult import com.paypal.android.corepayments.CoreConfig -import com.paypal.android.corepayments.CoreCoroutineExceptionHandler import com.paypal.android.corepayments.PayPalSDKError import com.paypal.android.corepayments.analytics.AnalyticsService import kotlinx.coroutines.CoroutineDispatcher @@ -40,12 +39,6 @@ class CardClient internal constructor( private var approveOrderId: String? = null - // TODO: remove once try-catch is removed - private val vaultExceptionHandler = CoreCoroutineExceptionHandler { error -> - analytics.notifyVaultUnknownError(null) - cardVaultListener?.onVaultFailure(error) - } - /** * CardClient constructor * @@ -110,28 +103,31 @@ class CardClient internal constructor( analytics.notifyVaultStarted(cardVaultRequest.setupTokenId) val applicationContext = context.applicationContext - CoroutineScope(dispatcher).launch(vaultExceptionHandler) { - try { - val updateSetupTokenResult = cardVaultRequest.run { - paymentMethodTokensAPI.updateSetupToken(applicationContext, setupTokenId, card) + CoroutineScope(dispatcher).launch { + val updateSetupTokenResult = cardVaultRequest.run { + paymentMethodTokensAPI.updateSetupToken(applicationContext, setupTokenId, card) + } + when (updateSetupTokenResult) { + is UpdateSetupTokenResult.Success -> { + val approveHref = updateSetupTokenResult.approveHref + if (approveHref == null) { + analytics.notifyVaultSucceeded(updateSetupTokenResult.setupTokenId) + val result = + updateSetupTokenResult.run { CardVaultResult(setupTokenId, status) } + cardVaultListener?.onVaultSuccess(result) + } else { + analytics.notifyVaultAuthChallengeReceived(updateSetupTokenResult.setupTokenId) + val url = Uri.parse(approveHref) + val authChallenge = + CardAuthChallenge.Vault(url = url, request = cardVaultRequest) + cardVaultListener?.onVaultAuthorizationRequired(authChallenge) + } } - val approveHref = updateSetupTokenResult.approveHref - if (approveHref == null) { - analytics.notifyVaultSucceeded(updateSetupTokenResult.setupTokenId) - val result = - updateSetupTokenResult.run { CardVaultResult(setupTokenId, status) } - cardVaultListener?.onVaultSuccess(result) - } else { - analytics.notifyVaultAuthChallengeReceived(updateSetupTokenResult.setupTokenId) - val url = Uri.parse(approveHref) - val authChallenge = - CardAuthChallenge.Vault(url = url, request = cardVaultRequest) - cardVaultListener?.onVaultAuthorizationRequired(authChallenge) + is UpdateSetupTokenResult.Failure -> { + analytics.notifyVaultFailed(cardVaultRequest.setupTokenId) + cardVaultListener?.onVaultFailure(updateSetupTokenResult.error) } - } catch (error: PayPalSDKError) { - analytics.notifyVaultFailed(cardVaultRequest.setupTokenId) - throw error } } } diff --git a/CardPayments/src/main/java/com/paypal/android/cardpayments/CardError.kt b/CardPayments/src/main/java/com/paypal/android/cardpayments/CardError.kt index 4f751f0c6..196624840 100644 --- a/CardPayments/src/main/java/com/paypal/android/cardpayments/CardError.kt +++ b/CardPayments/src/main/java/com/paypal/android/cardpayments/CardError.kt @@ -1,6 +1,7 @@ package com.paypal.android.cardpayments import com.paypal.android.corepayments.PayPalSDKError +import com.paypal.android.corepayments.graphql.GraphQLError internal object CardError { @@ -27,4 +28,10 @@ internal object CardError { code = CardErrorCode.BROWSER_SWITCH.ordinal, errorDescription = cause.message ?: "Unable to Browser Switch" ) + + fun updateSetupTokenResponseBodyMissing(errors: List?, correlationId: String?) = PayPalSDKError( + 0, + "Error updating setup token: $errors", + correlationId + ) } diff --git a/CardPayments/src/main/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPI.kt b/CardPayments/src/main/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPI.kt index a5a949371..e063f1910 100644 --- a/CardPayments/src/main/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPI.kt +++ b/CardPayments/src/main/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPI.kt @@ -2,10 +2,13 @@ package com.paypal.android.cardpayments import android.content.Context import com.paypal.android.corepayments.CoreConfig +import com.paypal.android.corepayments.LoadRawResourceResult import com.paypal.android.corepayments.PayPalSDKError import com.paypal.android.corepayments.ResourceLoader import com.paypal.android.corepayments.graphql.GraphQLClient +import com.paypal.android.corepayments.graphql.GraphQLResult import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject internal class DataVaultPaymentMethodTokensAPI internal constructor( @@ -20,8 +23,23 @@ internal class DataVaultPaymentMethodTokensAPI internal constructor( ResourceLoader() ) - suspend fun updateSetupToken(context: Context, setupTokenId: String, card: Card): UpdateSetupTokenResult { - val query = resourceLoader.loadRawResource(context, R.raw.graphql_query_update_setup_token) + suspend fun updateSetupToken( + context: Context, + setupTokenId: String, + card: Card + ): UpdateSetupTokenResult = when (val result = + resourceLoader.loadRawResource(context, R.raw.graphql_query_update_setup_token)) { + is LoadRawResourceResult.Success -> + sendUpdateSetupTokenGraphQLRequest(result.value, setupTokenId, card) + + is LoadRawResourceResult.Failure -> UpdateSetupTokenResult.Failure(result.error) + } + + private suspend fun sendUpdateSetupTokenGraphQLRequest( + query: String, + setupTokenId: String, + card: Card + ): UpdateSetupTokenResult { val cardNumber = card.number.replace("\\s".toRegex(), "") val cardExpiry = "${card.expirationYear}-${card.expirationMonth}" @@ -57,33 +75,55 @@ internal class DataVaultPaymentMethodTokensAPI internal constructor( .put("variables", variables) val graphQLResponse = graphQLClient.send(graphQLRequest, queryName = "UpdateVaultSetupToken") - graphQLResponse.data?.let { responseJSON -> - val setupTokenJSON = responseJSON.getJSONObject("updateVaultSetupToken") + return when (graphQLResponse) { + is GraphQLResult.Success -> { + val responseJSON = graphQLResponse.data + if (responseJSON == null) { + val error = graphQLResponse.run { + CardError.updateSetupTokenResponseBodyMissing(errors, correlationId) + } + UpdateSetupTokenResult.Failure(error) + } else { + parseSuccessfulUpdateSuccessJSON(responseJSON, graphQLResponse.correlationId) + } + } + + is GraphQLResult.Failure -> { + UpdateSetupTokenResult.Failure(graphQLResponse.error) + } + } + } + + private fun parseSuccessfulUpdateSuccessJSON( + responseBody: JSONObject, + correlationId: String? + ): UpdateSetupTokenResult { + return try { + val setupTokenJSON = responseBody.getJSONObject("updateVaultSetupToken") val status = setupTokenJSON.getString("status") val approveHref = if (status == "PAYER_ACTION_REQUIRED") { findLinkHref(setupTokenJSON, "approve") } else { null } - return UpdateSetupTokenResult( + UpdateSetupTokenResult.Success( setupTokenId = setupTokenJSON.getString("id"), status = status, approveHref = approveHref ) + } catch (jsonError: JSONException) { + val message = "Update Setup Token Failed: GraphQL JSON body was invalid." + val error = PayPalSDKError(0, message, correlationId, reason = jsonError) + UpdateSetupTokenResult.Failure(error) } - throw PayPalSDKError( - 0, - "Error updating setup token: ${graphQLResponse.errors}", - graphQLResponse.correlationId - ) } private fun findLinkHref(responseJSON: JSONObject, rel: String): String? { val linksJSON = responseJSON.optJSONArray("links") ?: JSONArray() for (i in 0 until linksJSON.length()) { val link = linksJSON.getJSONObject(i) - if (link.getString("rel") == rel) { - return link.getString("href") + if (link.optString("rel") == rel) { + return link.optString("href") } } return null diff --git a/CardPayments/src/main/java/com/paypal/android/cardpayments/UpdateSetupTokenResult.kt b/CardPayments/src/main/java/com/paypal/android/cardpayments/UpdateSetupTokenResult.kt index b9dea40d4..c813c310a 100644 --- a/CardPayments/src/main/java/com/paypal/android/cardpayments/UpdateSetupTokenResult.kt +++ b/CardPayments/src/main/java/com/paypal/android/cardpayments/UpdateSetupTokenResult.kt @@ -1,7 +1,14 @@ package com.paypal.android.cardpayments -internal data class UpdateSetupTokenResult( - val setupTokenId: String, - val status: String, - val approveHref: String? -) +import com.paypal.android.corepayments.PayPalSDKError + +internal sealed class UpdateSetupTokenResult { + + data class Success( + val setupTokenId: String, + val status: String, + val approveHref: String? + ) : UpdateSetupTokenResult() + + data class Failure(val error: PayPalSDKError) : UpdateSetupTokenResult() +} diff --git a/CardPayments/src/test/java/com/paypal/android/cardpayments/CardClientUnitTest.kt b/CardPayments/src/test/java/com/paypal/android/cardpayments/CardClientUnitTest.kt index b95568fb0..0cbd87d39 100644 --- a/CardPayments/src/test/java/com/paypal/android/cardpayments/CardClientUnitTest.kt +++ b/CardPayments/src/test/java/com/paypal/android/cardpayments/CardClientUnitTest.kt @@ -148,7 +148,7 @@ class CardClientUnitTest { val sut = createCardClient(testScheduler) val updateSetupTokenResult = - UpdateSetupTokenResult("fake-setup-token-id-from-result", "fake-status", null) + UpdateSetupTokenResult.Success("fake-setup-token-id-from-result", "fake-status", null) coEvery { paymentMethodTokensAPI.updateSetupToken(applicationContext, "fake-setup-token-id", card) } returns updateSetupTokenResult @@ -171,7 +171,7 @@ class CardClientUnitTest { val error = PayPalSDKError(0, "mock_error_message") coEvery { paymentMethodTokensAPI.updateSetupToken(applicationContext, "fake-setup-token-id", card) - } throws error + } returns UpdateSetupTokenResult.Failure(error) sut.vault(activity, cardVaultRequest) advanceUntilIdle() @@ -187,7 +187,7 @@ class CardClientUnitTest { fun `vault notifies listener when authorization is required`() = runTest { val sut = createCardClient(testScheduler) - val updateSetupTokenResult = UpdateSetupTokenResult( + val updateSetupTokenResult = UpdateSetupTokenResult.Success( "fake-setup-token-id-from-result", "fake-status", "/payer/action/href" diff --git a/CardPayments/src/test/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPIUnitTest.kt b/CardPayments/src/test/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPIUnitTest.kt index 551e28242..ebb797be9 100644 --- a/CardPayments/src/test/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPIUnitTest.kt +++ b/CardPayments/src/test/java/com/paypal/android/cardpayments/DataVaultPaymentMethodTokensAPIUnitTest.kt @@ -5,18 +5,21 @@ import androidx.test.core.app.ApplicationProvider import com.paypal.android.corepayments.Address import com.paypal.android.corepayments.CoreConfig import com.paypal.android.corepayments.Environment +import com.paypal.android.corepayments.LoadRawResourceResult import com.paypal.android.corepayments.ResourceLoader import com.paypal.android.corepayments.graphql.GraphQLClient -import com.paypal.android.corepayments.graphql.GraphQLResponse +import com.paypal.android.corepayments.graphql.GraphQLResult import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.json.JSONException import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -32,36 +35,41 @@ class DataVaultPaymentMethodTokensAPIUnitTest { private val resourceLoader = ResourceLoader() private val context = ApplicationProvider.getApplicationContext() + private lateinit var card: Card private lateinit var graphQLClient: GraphQLClient + private lateinit var sut: DataVaultPaymentMethodTokensAPI @Before fun beforeEach() { + card = Card( + number = "4111111111111111", + expirationMonth = "01", + expirationYear = "24", + securityCode = "123", + cardholderName = "Jane Doe", + ) graphQLClient = mockk(relaxed = true) } @Test fun updateSetupToken_forCardWithRequiredFieldsSet_sendsGraphQLRequest() = runTest { sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) - - val card = Card( - number = "4111111111111111", - expirationMonth = "01", - expirationYear = "24", - securityCode = "123" - ) sut.updateSetupToken(context, "fake-setup-token-id", card) val requestBodySlot = slot() coVerify { graphQLClient.send(capture(requestBodySlot), "UpdateVaultSetupToken") } val actualRequestBody = requestBodySlot.captured - val expectedQuery = - resourceLoader.loadRawResource(context, R.raw.graphql_query_update_setup_token) + val expectedQuery = resourceLoader.loadRawResource( + context, + R.raw.graphql_query_update_setup_token + ) as LoadRawResourceResult.Success + // language=JSON val expectedRequestBody = """ { - "query": "$expectedQuery", + "query": "${expectedQuery.value}", "variables": { "clientId": "fake-client-id", "vaultSetupToken": "fake-setup-token-id", @@ -69,6 +77,7 @@ class DataVaultPaymentMethodTokensAPIUnitTest { "card": { "number": "4111111111111111", "expiry": "24-01", + "name": "Jane Doe", "securityCode": "123" } } @@ -81,36 +90,31 @@ class DataVaultPaymentMethodTokensAPIUnitTest { @Test fun updateSetupToken_forCardWithRequiredAndOptionalFieldsSet_sendsGraphQLRequest() = runTest { - sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) - - val card = Card( - number = "4111111111111111", - expirationMonth = "01", - expirationYear = "24", - securityCode = "123", - cardholderName = "Jane Doe", - billingAddress = Address( - streetAddress = "2211 N 1st St.", - extendedAddress = "Apt. 1A", - locality = "San Jose", - region = "CA", - postalCode = "95131", - countryCode = "US" - ) + card.billingAddress = Address( + streetAddress = "2211 N 1st St.", + extendedAddress = "Apt. 1A", + locality = "San Jose", + region = "CA", + postalCode = "95131", + countryCode = "US" ) + sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) sut.updateSetupToken(context, "fake-setup-token-id", card) val requestBodySlot = slot() coVerify { graphQLClient.send(capture(requestBodySlot), "UpdateVaultSetupToken") } val actualRequestBody = requestBodySlot.captured - val expectedQuery = - resourceLoader.loadRawResource(context, R.raw.graphql_query_update_setup_token) + val expectedQuery = resourceLoader.loadRawResource( + context, + R.raw.graphql_query_update_setup_token + ) as LoadRawResourceResult.Success + // language=JSON val expectedRequestBody = """ { - "query": "$expectedQuery", + "query": "${expectedQuery.value}", "variables": { "clientId": "fake-client-id", "vaultSetupToken": "fake-setup-token-id", @@ -152,17 +156,12 @@ class DataVaultPaymentMethodTokensAPIUnitTest { } } """.trimIndent() - val graphQLResponse = GraphQLResponse(JSONObject(json)) - coEvery { graphQLClient.send(any(), "UpdateVaultSetupToken") } returns graphQLResponse + val graphQLResult = GraphQLResult.Success(JSONObject(json)) + coEvery { graphQLClient.send(any(), "UpdateVaultSetupToken") } returns graphQLResult - val card = Card( - number = "4111111111111111", - expirationMonth = "01", - expirationYear = "24", - securityCode = "123" - ) sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) val result = sut.updateSetupToken(context, "fake-setup-token-id", card) + as UpdateSetupTokenResult.Success assertEquals("fake-setup-token-id-from-result", result.setupTokenId) assertEquals("APPROVED", result.status) @@ -184,20 +183,57 @@ class DataVaultPaymentMethodTokensAPIUnitTest { } } """.trimIndent() - val graphQLResponse = GraphQLResponse(JSONObject(json)) - coEvery { graphQLClient.send(any(), "UpdateVaultSetupToken") } returns graphQLResponse + val graphQLResult = GraphQLResult.Success(JSONObject(json)) + coEvery { graphQLClient.send(any(), "UpdateVaultSetupToken") } returns graphQLResult - val card = Card( - number = "4111111111111111", - expirationMonth = "01", - expirationYear = "24", - securityCode = "123" - ) sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) val result = sut.updateSetupToken(context, "fake-setup-token-id", card) + as UpdateSetupTokenResult.Success assertEquals("fake-setup-token-id-from-result", result.setupTokenId) assertEquals("PAYER_ACTION_REQUIRED", result.status) assertEquals("https://fake.com/approval/url", result.approveHref) } + + @Test + fun updateSetupToken_returnsFailureWhenUpdateVaultSetupTokenFieldIsMissing() = runTest { + // language=JSON + val emptyJSON = """{}""".trimIndent() + val graphQLResult = + GraphQLResult.Success(JSONObject(emptyJSON), correlationId = "fake-correlation-id") + coEvery { graphQLClient.send(any(), "UpdateVaultSetupToken") } returns graphQLResult + + sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) + val result = sut.updateSetupToken(context, "fake-setup-token-id", card) + as UpdateSetupTokenResult.Failure + + val expectedMessage = "Update Setup Token Failed: GraphQL JSON body was invalid." + assertEquals(expectedMessage, result.error.errorDescription) + assertEquals("fake-correlation-id", result.error.correlationId) + assertTrue(result.error.cause is JSONException) + } + + @Test + fun updateSetupToken_returnsFailureWhenStatusFieldIsMissing() = runTest { + // language=JSON + val json = """ + { + "updateVaultSetupToken": { + "id": "fake-setup-token-id-from-result" + } + } + """.trimIndent() + val graphQLResult = + GraphQLResult.Success(JSONObject(json), correlationId = "fake-correlation-id") + coEvery { graphQLClient.send(any(), "UpdateVaultSetupToken") } returns graphQLResult + + sut = DataVaultPaymentMethodTokensAPI(coreConfig, graphQLClient, resourceLoader) + val result = sut.updateSetupToken(context, "fake-setup-token-id", card) + as UpdateSetupTokenResult.Failure + + val expectedMessage = "Update Setup Token Failed: GraphQL JSON body was invalid." + assertEquals(expectedMessage, result.error.errorDescription) + assertEquals("fake-correlation-id", result.error.correlationId) + assertTrue(result.error.cause is JSONException) + } } diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/APIClientError.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/APIClientError.kt index 990e24541..6ff451dd0 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/APIClientError.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/APIClientError.kt @@ -1,7 +1,6 @@ package com.paypal.android.corepayments import androidx.annotation.RestrictTo -import java.lang.Exception /** * @suppress @@ -80,4 +79,16 @@ object APIClientError { errorDescription = "Error fetching clientId. Contact developer.paypal.com/support.", correlationId = correlationId ) + + fun graphQLJSONParseError(correlationId: String?, reason: Exception): PayPalSDKError { + val message = + "An error occurred while parsing the GraphQL response JSON. Contact developer.paypal.com/support." + val error = PayPalSDKError( + code = PayPalSDKErrorCode.GRAPHQL_JSON_INVALID_ERROR.ordinal, + errorDescription = message, + correlationId = correlationId, + reason = reason + ) + return error + } } diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/LoadRawResourceResult.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/LoadRawResourceResult.kt new file mode 100644 index 000000000..5d1ff3209 --- /dev/null +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/LoadRawResourceResult.kt @@ -0,0 +1,6 @@ +package com.paypal.android.corepayments + +sealed class LoadRawResourceResult { + data class Success(val value: String) : LoadRawResourceResult() + data class Failure(val error: PayPalSDKError) : LoadRawResourceResult() +} diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/PayPalSDKErrorCode.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/PayPalSDKErrorCode.kt index ffe900d8a..ea5717312 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/PayPalSDKErrorCode.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/PayPalSDKErrorCode.kt @@ -14,5 +14,6 @@ enum class PayPalSDKErrorCode { INVALID_URL_REQUEST, SERVER_RESPONSE_ERROR, CHECKOUT_ERROR, - NATIVE_CHECKOUT_ERROR + NATIVE_CHECKOUT_ERROR, + GRAPHQL_JSON_INVALID_ERROR } diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/ResourceLoader.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/ResourceLoader.kt index b469dc9a0..10ad0025f 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/ResourceLoader.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/ResourceLoader.kt @@ -21,19 +21,22 @@ class ResourceLoader { * @param context Android context * @param resId ID of the resource that will be loaded */ - suspend fun loadRawResource(context: Context, @RawRes resId: Int): String = withContext(Dispatchers.IO) { - try { - val resInputStream = context.resources.openRawResource(resId) - val resAsBytes = ByteArray(resInputStream.available()) - resInputStream.read(resAsBytes) - resInputStream.close() - String(resAsBytes) - } catch (e: Resources.NotFoundException) { - val errorDescription = "Resource with id $resId not found." - throw PayPalSDKError(0, errorDescription, reason = e) - } catch (e: IOException) { - val errorDescription = "Error loading resource with id $resId." - throw PayPalSDKError(0, errorDescription, reason = e) + suspend fun loadRawResource(context: Context, @RawRes resId: Int): LoadRawResourceResult = + withContext(Dispatchers.IO) { + try { + val resInputStream = context.resources.openRawResource(resId) + val resAsBytes = ByteArray(resInputStream.available()) + resInputStream.read(resAsBytes) + resInputStream.close() + LoadRawResourceResult.Success(String(resAsBytes)) + } catch (e: Resources.NotFoundException) { + val errorDescription = "Resource with id $resId not found." + val resourceNotFoundError = PayPalSDKError(0, errorDescription, reason = e) + LoadRawResourceResult.Failure(resourceNotFoundError) + } catch (e: IOException) { + val errorDescription = "Error loading resource with id $resId." + val ioError = PayPalSDKError(0, errorDescription, reason = e) + LoadRawResourceResult.Failure(ioError) + } } - } } diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt index 1fba576ac..d5ad6c970 100644 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLClient.kt @@ -6,6 +6,7 @@ import com.paypal.android.corepayments.CoreConfig import com.paypal.android.corepayments.Http import com.paypal.android.corepayments.HttpMethod import com.paypal.android.corepayments.HttpRequest +import org.json.JSONException import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL @@ -35,7 +36,7 @@ class GraphQLClient internal constructor( "Origin" to coreConfig.environment.graphQLEndpoint ) - suspend fun send(graphQLRequestBody: JSONObject, queryName: String? = null): GraphQLResponse { + suspend fun send(graphQLRequestBody: JSONObject, queryName: String? = null): GraphQLResult { val body = graphQLRequestBody.toString() val urlString = if (queryName != null) "$graphQLURL?$queryName" else graphQLURL val httpRequest = HttpRequest(URL(urlString), HttpMethod.POST, body, httpRequestHeaders) @@ -45,13 +46,19 @@ class GraphQLClient internal constructor( val status = httpResponse.status return if (status == HttpURLConnection.HTTP_OK) { if (httpResponse.body.isNullOrBlank()) { - throw APIClientError.noResponseData(correlationId) + val error = APIClientError.noResponseData(correlationId) + GraphQLResult.Failure(error) } else { - val responseAsJSON = JSONObject(httpResponse.body) - GraphQLResponse(responseAsJSON.getJSONObject("data"), correlationId = correlationId) + try { + val responseAsJSON = JSONObject(httpResponse.body) + GraphQLResult.Success(responseAsJSON.getJSONObject("data"), correlationId = correlationId) + } catch (jsonParseError: JSONException) { + val error = APIClientError.graphQLJSONParseError(correlationId, jsonParseError) + GraphQLResult.Failure(error) + } } } else { - GraphQLResponse(null, correlationId = correlationId) + GraphQLResult.Success(null, correlationId = correlationId) } } } diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLResponse.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLResponse.kt deleted file mode 100644 index 2b314c513..000000000 --- a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLResponse.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.paypal.android.corepayments.graphql - -import androidx.annotation.RestrictTo -import org.json.JSONObject - -/** - * @suppress - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -data class GraphQLResponse( - val data: JSONObject? = null, - val extensions: List? = null, - val errors: List? = null, - val correlationId: String? = null -) diff --git a/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLResult.kt b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLResult.kt new file mode 100644 index 000000000..85481c5bc --- /dev/null +++ b/CorePayments/src/main/java/com/paypal/android/corepayments/graphql/GraphQLResult.kt @@ -0,0 +1,21 @@ +package com.paypal.android.corepayments.graphql + +import androidx.annotation.RestrictTo +import com.paypal.android.corepayments.PayPalSDKError +import org.json.JSONObject + +/** + * @suppress + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +sealed class GraphQLResult { + + data class Success( + val data: JSONObject? = null, + val extensions: List? = null, + val errors: List? = null, + val correlationId: String? = null + ) : GraphQLResult() + + data class Failure(val error: PayPalSDKError) : GraphQLResult() +} diff --git a/CorePayments/src/test/java/com/paypal/android/corepayments/graphql/GraphQLClientUnitTest.kt b/CorePayments/src/test/java/com/paypal/android/corepayments/graphql/GraphQLClientUnitTest.kt index b6cd0f27a..01f2abb6f 100644 --- a/CorePayments/src/test/java/com/paypal/android/corepayments/graphql/GraphQLClientUnitTest.kt +++ b/CorePayments/src/test/java/com/paypal/android/corepayments/graphql/GraphQLClientUnitTest.kt @@ -6,7 +6,6 @@ import com.paypal.android.corepayments.Http import com.paypal.android.corepayments.HttpMethod import com.paypal.android.corepayments.HttpRequest import com.paypal.android.corepayments.HttpResponse -import com.paypal.android.corepayments.PayPalSDKError import com.paypal.android.corepayments.PayPalSDKErrorCode import io.mockk.CapturingSlot import io.mockk.coEvery @@ -128,14 +127,14 @@ internal class GraphQLClientUnitTest { coEvery { http.send(any()) } returns successHttpResponse sut = GraphQLClient(sandboxConfig, http) - val response = sut.send(graphQLRequestBody) + val result = sut.send(graphQLRequestBody) as GraphQLResult.Success - assertEquals("""{"fake":"success_data"}""", response.data?.toString()) - assertEquals("fake-debug-id", response.correlationId) + assertEquals("""{"fake":"success_data"}""", result.data?.toString()) + assertEquals("fake-debug-id", result.correlationId) } @Test - fun `send throws an error when GraphQL response is successful with an empty body`() = runTest { + fun `send returns an error when GraphQL response is successful with an empty body`() = runTest { // language=JSON val emptyBody = "" val successHeaders = mapOf("Paypal-Debug-Id" to "fake-debug-id") @@ -143,14 +142,30 @@ internal class GraphQLClientUnitTest { coEvery { http.send(any()) } returns successHttpResponse sut = GraphQLClient(sandboxConfig, http) - try { - sut.send(graphQLRequestBody) - } catch (e: PayPalSDKError) { - assertEquals(PayPalSDKErrorCode.NO_RESPONSE_DATA.ordinal, e.code) + val result = sut.send(graphQLRequestBody) as GraphQLResult.Failure + assertEquals(PayPalSDKErrorCode.NO_RESPONSE_DATA.ordinal, result.error.code) + + val expectedErrorMessage = + "An error occurred due to missing HTTP response data. Contact developer.paypal.com/support." + assertEquals(expectedErrorMessage, result.error.errorDescription) + assertEquals("fake-debug-id", result.error.correlationId) + } + + @Test + fun `send returns an error when GraphQL response is successful with an invalid JSON body`() = + runTest { + val invalidJSON = """{ invalid: """ + val successHeaders = mapOf("Paypal-Debug-Id" to "fake-debug-id") + val successHttpResponse = HttpResponse(200, successHeaders, invalidJSON) + coEvery { http.send(any()) } returns successHttpResponse + + sut = GraphQLClient(sandboxConfig, http) + val result = sut.send(graphQLRequestBody) as GraphQLResult.Failure + assertEquals(PayPalSDKErrorCode.GRAPHQL_JSON_INVALID_ERROR.ordinal, result.error.code) + val expectedErrorMessage = - "An error occurred due to missing HTTP response data. Contact developer.paypal.com/support." - assertEquals(expectedErrorMessage, e.errorDescription) - assertEquals("fake-debug-id", e.correlationId) + "An error occurred while parsing the GraphQL response JSON. Contact developer.paypal.com/support." + assertEquals(expectedErrorMessage, result.error.errorDescription) + assertEquals("fake-debug-id", result.error.correlationId) } - } }