From a9881d29b5232cc2869ecd4cdf7b9f1f025e4f22 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Fri, 2 Apr 2021 23:37:14 -0700 Subject: [PATCH 1/6] suspend funs and tests --- .../com/stripe/android/PaymentController.kt | 70 ++ .../main/java/com/stripe/android/Stripe.kt | 50 +- .../stripe/android/StripePaymentController.kt | 101 +++ .../java/com/stripe/android/StripeSuspend.kt | 682 ++++++++++++++++++ .../com/stripe/android/StripeKotlinTest.kt | 678 +++++++++++++++++ 5 files changed, 1572 insertions(+), 9 deletions(-) create mode 100644 stripe/src/main/java/com/stripe/android/StripeSuspend.kt create mode 100644 stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt diff --git a/stripe/src/main/java/com/stripe/android/PaymentController.kt b/stripe/src/main/java/com/stripe/android/PaymentController.kt index 59394abf99f..e846c7b34b4 100644 --- a/stripe/src/main/java/com/stripe/android/PaymentController.kt +++ b/stripe/src/main/java/com/stripe/android/PaymentController.kt @@ -1,6 +1,10 @@ package com.stripe.android import android.content.Intent +import com.stripe.android.exception.APIConnectionException +import com.stripe.android.exception.APIException +import com.stripe.android.exception.AuthenticationException +import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmStripeIntentParams import com.stripe.android.model.Source @@ -64,6 +68,28 @@ internal interface PaymentController { callback: ApiResultCallback ) + /** + * Get the PaymentIntent's client_secret from {@param data} and use to retrieve + * the PaymentIntent object with updated status. + * + * @param data the result Intent + * @return the [PaymentIntentResult] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws IllegalArgumentException if the PaymentIntent response's JsonParser returns null + */ + @Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class + ) + suspend fun getPaymentIntentResult(data: Intent): PaymentIntentResult + /** * If setup authentication triggered an exception, get the exception object and pass to * [ApiResultCallback.onError]. @@ -78,11 +104,55 @@ internal interface PaymentController { callback: ApiResultCallback ) + /** + * Get the SetupIntent's client_secret from {@param data} and use to retrieve + * the SetupIntent object with updated status. + * + * @param data the result Intent + * @return the [SetupIntentResult] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws IllegalArgumentException if the SetupIntent response's JsonParser returns null + */ + @Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class + ) + suspend fun getSetupIntentResult(data: Intent): SetupIntentResult + fun handleSourceResult( data: Intent, callback: ApiResultCallback ) + /** + * Get the Source's client_secret from {@param data} and use to retrieve + * the Source object with updated status. + * + * @param data the result Intent + * @return the [Source] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws IllegalArgumentException if the Source response's JsonParser returns null + */ + @Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class + ) + suspend fun getSource(data: Intent): Source + /** * Determine which authentication mechanism should be used, or bypass authentication * if it is not needed. diff --git a/stripe/src/main/java/com/stripe/android/Stripe.kt b/stripe/src/main/java/com/stripe/android/Stripe.kt index 50ebcd3537c..82a997f4c01 100644 --- a/stripe/src/main/java/com/stripe/android/Stripe.kt +++ b/stripe/src/main/java/com/stripe/android/Stripe.kt @@ -58,13 +58,13 @@ import kotlin.coroutines.CoroutineContext * */ class Stripe internal constructor( - private val stripeRepository: StripeRepository, - private val paymentController: PaymentController, + internal val stripeRepository: StripeRepository, + internal val paymentController: PaymentController, publishableKey: String, - private val stripeAccountId: String? = null, + internal val stripeAccountId: String? = null, private val workContext: CoroutineContext = Dispatchers.IO ) { - private val publishableKey: String = ApiKeyValidator().requireValid(publishableKey) + internal val publishableKey: String = ApiKeyValidator().requireValid(publishableKey) /** * Constructor with publishable key and Stripe Connect account id. @@ -322,6 +322,20 @@ class Stripe internal constructor( ) } + /** + * Check if the requestCode and intent is for [PaymentIntentResult]. + * The Intent should the retrieved from result from `Activity#onActivityResult(int, int, Intent)}}` + * by activity started with [confirmPayment] or [handleNextActionForPayment]. + * + * @return whether the requestCode and intent is for [PaymentIntentResult]. + */ + fun isForPaymentIntentResult( + requestCode: Int, + data: Intent? + ): Boolean { + return data != null && paymentController.shouldHandlePaymentResult(requestCode, data) + } + /** * Should be called via `Activity#onActivityResult(int, int, Intent)}}` to handle the * result of a PaymentIntent automatic confirmation (see [confirmPayment]) or @@ -333,8 +347,8 @@ class Stripe internal constructor( data: Intent?, callback: ApiResultCallback ): Boolean { - return if (data != null && paymentController.shouldHandlePaymentResult(requestCode, data)) { - paymentController.handlePaymentResult(data, callback) + return if (isForPaymentIntentResult(requestCode, data)) { + paymentController.handlePaymentResult(data!!, callback) true } else { false @@ -597,6 +611,20 @@ class Stripe internal constructor( ) } + /** + * Check if the requestCode and intent is for [SetupIntentResult]. + * The Intent should the retrieved from result from `Activity#onActivityResult(int, int, Intent)}}` + * by activity started with [confirmSetupIntent]. + * + * @return whether the requestCode and intent is for [SetupIntentResult]. + */ + fun isForSetupIntentResult( + requestCode: Int, + data: Intent? + ): Boolean { + return data != null && paymentController.shouldHandleSetupResult(requestCode, data) + } + /** * Should be called via `Activity#onActivityResult(int, int, Intent)}}` to handle the * result of a SetupIntent confirmation (see [confirmSetupIntent]). @@ -607,8 +635,8 @@ class Stripe internal constructor( data: Intent?, callback: ApiResultCallback ): Boolean { - return if (data != null && paymentController.shouldHandleSetupResult(requestCode, data)) { - paymentController.handleSetupResult(data, callback) + return if (isForSetupIntentResult(requestCode, data)) { + paymentController.handleSetupResult(data!!, callback) true } else { false @@ -850,7 +878,11 @@ class Stripe internal constructor( } /** - * Should be called in `onActivityResult()` to determine if the result is for Source authentication + * Check if the requestCode and intent is for [Source] authentication. + * The Intent should the retrieved from result from `Activity#onActivityResult(int, int, Intent)}}` + * by activity started with [authenticateSource]. + * + * @return whether the requestCode and intent is for [Source] authentication */ fun isAuthenticateSourceResult( requestCode: Int, diff --git a/stripe/src/main/java/com/stripe/android/StripePaymentController.kt b/stripe/src/main/java/com/stripe/android/StripePaymentController.kt index 3c66cf7048a..fcff6c47f5f 100644 --- a/stripe/src/main/java/com/stripe/android/StripePaymentController.kt +++ b/stripe/src/main/java/com/stripe/android/StripePaymentController.kt @@ -5,6 +5,10 @@ import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.annotation.VisibleForTesting import com.stripe.android.auth.PaymentAuthWebViewContract +import com.stripe.android.exception.APIConnectionException +import com.stripe.android.exception.APIException +import com.stripe.android.exception.AuthenticationException +import com.stripe.android.exception.InvalidRequestException import com.stripe.android.exception.StripeException import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams @@ -392,6 +396,31 @@ internal class StripePaymentController internal constructor( } } + /** + * Get the PaymentIntent's client_secret from {@param data} and use to retrieve + * the PaymentIntent object with updated status. + * + * @param data the result Intent + * @return the [PaymentIntentResult] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws IllegalArgumentException if the response's JsonParser returns null + */ + @Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class + ) + override suspend fun getPaymentIntentResult(data: Intent) = + paymentFlowResultProcessor.processPaymentIntent( + PaymentFlowResult.Unvalidated.fromIntent(data) + ) + /** * If setup authentication triggered an exception, get the exception object and pass to * [ApiResultCallback.onError]. @@ -421,6 +450,31 @@ internal class StripePaymentController internal constructor( } } + /** + * Get the SetupIntent's client_secret from {@param data} and use to retrieve + * the PaymentIntent object with updated status. + * + * @param data the result Intent + * @return the [SetupIntentResult] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws IllegalArgumentException if the response's JsonParser returns null + */ + @Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class + ) + override suspend fun getSetupIntentResult(data: Intent) = + paymentFlowResultProcessor.processSetupIntent( + PaymentFlowResult.Unvalidated.fromIntent(data) + ) + override fun handleSourceResult( data: Intent, callback: ApiResultCallback @@ -461,6 +515,53 @@ internal class StripePaymentController internal constructor( } } + /** + * Get the Source's client_secret from {@param data} and use to retrieve + * the Source object with updated status. + * + * @param data the result Intent + * @return the [Source] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws IllegalArgumentException if the Source response's JsonParser returns null + */ + @Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class + ) + override suspend fun getSource(data: Intent): Source { + val result = PaymentFlowResult.Unvalidated.fromIntent(data) + val sourceId = result.sourceId.orEmpty() + val clientSecret = result.clientSecret.orEmpty() + + val requestOptions = ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = result.stripeAccountId + ) + + analyticsRequestExecutor.executeAsync( + analyticsRequestFactory.create( + analyticsDataFactory.createAuthSourceParams( + AnalyticsEvent.AuthSourceResult, + sourceId + ) + ) + ) + return requireNotNull( + stripeRepository.retrieveSource( + sourceId, + clientSecret, + requestOptions + ) + ) + } + @VisibleForTesting internal suspend fun authenticateAlipay( paymentIntent: PaymentIntent, diff --git a/stripe/src/main/java/com/stripe/android/StripeSuspend.kt b/stripe/src/main/java/com/stripe/android/StripeSuspend.kt new file mode 100644 index 00000000000..d2ede78dc83 --- /dev/null +++ b/stripe/src/main/java/com/stripe/android/StripeSuspend.kt @@ -0,0 +1,682 @@ +package com.stripe.android + +import android.content.Intent +import androidx.annotation.Size +import com.stripe.android.exception.APIConnectionException +import com.stripe.android.exception.APIException +import com.stripe.android.exception.AuthenticationException +import com.stripe.android.exception.CardException +import com.stripe.android.exception.InvalidRequestException +import com.stripe.android.model.AccountParams +import com.stripe.android.model.BankAccountTokenParams +import com.stripe.android.model.CardParams +import com.stripe.android.model.ConfirmPaymentIntentParams +import com.stripe.android.model.ConfirmSetupIntentParams +import com.stripe.android.model.CvcTokenParams +import com.stripe.android.model.PaymentIntent +import com.stripe.android.model.PaymentMethod +import com.stripe.android.model.PaymentMethodCreateParams +import com.stripe.android.model.PersonTokenParams +import com.stripe.android.model.PiiTokenParams +import com.stripe.android.model.SetupIntent +import com.stripe.android.model.Source +import com.stripe.android.model.SourceParams +import com.stripe.android.model.StripeFile +import com.stripe.android.model.StripeFileParams +import com.stripe.android.model.Token +import com.stripe.android.networking.ApiRequest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Create a [PaymentMethod] from a coroutine. + * + * See [Create a PaymentMethod](https://stripe.com/docs/api/payment_methods/create). + * `POST /v1/payment_methods` + * + * @param paymentMethodCreateParams the [PaymentMethodCreateParams] to be used + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [PaymentMethod] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.createPaymentMethod( + paymentMethodCreateParams: PaymentMethodCreateParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createPaymentMethod( + paymentMethodCreateParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a [Source] from a coroutine. + * + * See [Create a source](https://stripe.com/docs/api/sources/create). + * `POST /v1/sources` + * + * @param sourceParams the [SourceParams] to be used + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Source] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.createSource( + sourceParams: SourceParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createSource( + sourceParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a [Token] from a coroutine. + * + * See [Create an account token](https://stripe.com/docs/api/tokens/create_account). + * `POST /v1/tokens` + * + * @param accountParams the [AccountParams] used to create this token + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Token] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, +) +suspend fun Stripe.createAccountToken( + accountParams: AccountParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createToken( + accountParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a bank account token from a coroutine. + * + * See [Create a bank account token](https://stripe.com/docs/api/tokens/create_bank_account). + * `POST /v1/tokens` + * + * @param bankAccountTokenParams the [BankAccountTokenParams] used to create this token + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Token] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, +) +suspend fun Stripe.createBankAccountToken( + bankAccountTokenParams: BankAccountTokenParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createToken( + bankAccountTokenParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a PII token from a coroutine. + * + * See [Create a PII account token](https://stripe.com/docs/api/tokens/create_pii). + * `POST /v1/tokens` + * + * @param personalId the personal id used to create this token + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Token] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, +) +suspend fun Stripe.createPiiToken( + personalId: String, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createToken( + PiiTokenParams(personalId), + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a Card token from a coroutine. + * + * See [Create a card token](https://stripe.com/docs/api/tokens/create_card). + * `POST /v1/tokens` + * + * @param cardParams the [CardParams] used to create this payment token + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Token] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws CardException the card cannot be charged for some reason + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + CardException::class +) +suspend fun Stripe.createCardToken( + cardParams: CardParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createToken( + cardParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a CVC update token from a coroutine. + * + * `POST /v1/tokens` + * + * @param cvc the CVC used to create this token + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Token] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, +) +suspend fun Stripe.createCvcUpdateToken( + @Size(min = 3, max = 4) cvc: String, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createToken( + CvcTokenParams(cvc), + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Creates a single-use token that represents the details for a person. Use this when creating or + * updating persons associated with a Connect account. + * See [the documentation](https://stripe.com/docs/connect/account-tokens) to learn more. + * + * See [Create a person token](https://stripe.com/docs/api/tokens/create_person) + * + * @param params the person token creation params + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Token] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, +) +suspend fun Stripe.createPersonToken( + params: PersonTokenParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.createToken( + params, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Create a [StripeFile] from a coroutine. + * + * * See [Create a file](https://stripe.com/docs/api/files/create). + * `POST /v1/files` + * + * @param fileParams the [StripeFileParams] used to create the [StripeFile] + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [StripeFile] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + * @throws CardException the card cannot be charged for some reason + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + CardException::class +) +suspend fun Stripe.createFile( + fileParams: StripeFileParams, + idempotencyKey: String? = null, + stripeAccountId: String? = this.stripeAccountId, +) = runOnIOAndThrowIfNull { + stripeRepository.createFile( + fileParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Retrieve a [PaymentIntent] from a coroutine. + * + * See [Retrieve a PaymentIntent](https://stripe.com/docs/api/payment_intents/retrieve). + * `GET /v1/payment_intents/:id` + * + * @param clientSecret the client_secret with which to retrieve the [PaymentIntent] + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [PaymentIntent] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.retrievePaymentIntent( + clientSecret: String, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.retrievePaymentIntent( + clientSecret, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId + ) + ) +} + +/** + * Retrieve a [SetupIntent] asynchronously. + * + * See [Retrieve a SetupIntent](https://stripe.com/docs/api/setup_intents/retrieve). + * `GET /v1/setup_intents/:id` + * + * @param clientSecret the client_secret with which to retrieve the [SetupIntent] + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [SetupIntent] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.retrieveSetupIntent( + clientSecret: String, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.retrieveSetupIntent( + clientSecret, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId + ) + ) +} + +/** + * Retrieve a [Source] from a coroutine. + * + * See [Retrieve a source](https://stripe.com/docs/api/sources/retrieve). + * `GET /v1/sources/:id` + * + * @param sourceId the [Source.id] field of the desired Source object + * @param clientSecret the [Source.clientSecret] field of the desired Source object + * @param stripeAccountId Optional, the Connect account to associate with this request. + * By default, will use the Connect account that was used to instantiate the `Stripe` object, if specified. + * + * @return a [Source] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.retrieveSource( + @Size(min = 1) sourceId: String, + @Size(min = 1) clientSecret: String, + stripeAccountId: String? = this.stripeAccountId +) = runOnIOAndThrowIfNull { + stripeRepository.retrieveSource( + sourceId, + clientSecret, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId + ) + ) +} + +/** + * Suspend function to confirm a [SetupIntent] object. + * + * See [Confirm a SetupIntent](https://stripe.com/docs/api/setup_intents/confirm). + * `POST /v1/setup_intents/:id/confirm` + * + * @param confirmSetupIntentParams a set of params with which to confirm the Setup Intent + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * + * @return a [SetupIntent] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.confirmSetupIntentSuspend( + confirmSetupIntentParams: ConfirmSetupIntentParams, + idempotencyKey: String? = null +) = runOnIOAndThrowIfNull { + stripeRepository.confirmSetupIntent( + confirmSetupIntentParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Suspend function to confirm a [PaymentIntent] object. + * + * See [Confirm a PaymentIntent](https://stripe.com/docs/api/payment_intents/confirm). + * `POST /v1/payment_intents/:id/confirm` + * + * @param confirmPaymentIntentParams a set of params with which to confirm the PaymentIntent + * @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests) + * + * @return a [PaymentIntent] object + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.confirmPaymentIntentSuspend( + confirmPaymentIntentParams: ConfirmPaymentIntentParams, + idempotencyKey: String? = null +) = runOnIOAndThrowIfNull { + stripeRepository.confirmPaymentIntent( + confirmPaymentIntentParams, + ApiRequest.Options( + apiKey = publishableKey, + stripeAccount = stripeAccountId, + idempotencyKey = idempotencyKey + ) + ) +} + +/** + * Consume the empty result from Stripe's internal Json Parser, throw [APIException] for public API. + */ +internal suspend inline fun runOnIOAndThrowIfNull( + crossinline block: suspend () -> T? +): T = + withContext(Dispatchers.IO) { + block() ?: run { + throw APIException(message = "${T::class.java.simpleName}'s JsonParser returns null.") + } + } + +/** + * Get the [PaymentIntentResult] from [Intent] returned via + * Activity#onActivityResult(int, int, Intent)}} for PaymentIntent automatic confirmation + * (see [confirmPayment]) or manual confirmation (see [handleNextActionForPayment]}) + * + * @param requestCode [Int] code passed from Activity#onActivityResult + * @param data [Intent] intent from Activity#onActivityResult + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, +) +suspend fun Stripe.getPaymentIntentResult( + requestCode: Int, + data: Intent?, +): PaymentIntentResult { + return if ( + isForPaymentIntentResult(requestCode, data) + ) runOnIOAndThrowForIllegalArgumentException { paymentController.getPaymentIntentResult(data!!) } + else throw IllegalArgumentException("Intent is not from Stripe#confirmPayment or Stripe#handleNextActionForPayment.") +} + +/** + * Get the [SetupIntentResult] from [Intent] returned via + * Activity#onActivityResult(int, int, Intent)}} for SetupIntentResult confirmation. + * (see [confirmSetupIntent]) + * + * @param requestCode [Int] code passed from Activity#onActivityResult + * @param data [Intent] intent from Activity#onActivityResult + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class, + IllegalArgumentException::class +) +suspend fun Stripe.getSetupIntentResult( + requestCode: Int, + data: Intent?, +): SetupIntentResult { + return if ( + isForSetupIntentResult(requestCode, data) + ) runOnIOAndThrowForIllegalArgumentException { paymentController.getSetupIntentResult(data!!) } + else throw IllegalArgumentException("Intent is not from Stripe#confirmSetupIntent.") +} + +/** + * Get the [Source] from [Intent] returned via + * Activity#onActivityResult(int, int, Intent)}} for [Source] authentication. + * (see [authenticateSource]) + * + * @param requestCode [Int] code passed from Activity#onActivityResult + * @param data [Intent] intent from Activity#onActivityResult + * + * @throws AuthenticationException failure to properly authenticate yourself (check your key) + * @throws InvalidRequestException your request has invalid parameters + * @throws APIConnectionException failure to connect to Stripe's API + * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) + */ +@Throws( + AuthenticationException::class, + InvalidRequestException::class, + APIConnectionException::class, + APIException::class +) +suspend fun Stripe.getAuthenticateSourceResult( + requestCode: Int, + data: Intent?, +): Source { + return if ( + isAuthenticateSourceResult(requestCode, data) + ) runOnIOAndThrowForIllegalArgumentException { paymentController.getSource(data!!) } + else throw IllegalArgumentException("Intent is not from Stripe#authenticateSource.") +} + +/** + * Consume the [IllegalArgumentException] caused by empty result from Stripe's internal Json Parser, + * throw [APIException] for public API. + */ +internal suspend inline fun runOnIOAndThrowForIllegalArgumentException( + crossinline block: suspend () -> T +): T = + withContext(Dispatchers.IO) { + try { + block() + } catch (e: IllegalArgumentException) { + throw APIException(message = "${T::class.java.simpleName}'s JsonParser returns null.") + } + } diff --git a/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt b/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt new file mode 100644 index 00000000000..7470481ccf9 --- /dev/null +++ b/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt @@ -0,0 +1,678 @@ +package com.stripe.android + +import android.content.Intent +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.stripe.android.exception.APIException +import com.stripe.android.exception.AuthenticationException +import com.stripe.android.model.PaymentIntent +import com.stripe.android.model.SetupIntent +import com.stripe.android.model.Source +import com.stripe.android.networking.ApiRequest +import com.stripe.android.networking.StripeApiRepository +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.`when` +import kotlin.test.assertFailsWith +import kotlin.test.assertSame + +/** + * Test for [Stripe] suspend functions. + */ +@RunWith(JUnit4::class) +internal class StripeKotlinTest { + private val mockApiRepository: StripeApiRepository = mock() + private val mockPaymentController: PaymentController = mock() + + private val stripe: Stripe = + Stripe(mockApiRepository, mockPaymentController, TEST_PUBLISHABLE_KEY) + + @Test + fun `When repository returns correct value then createPaymentMethod should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createPaymentMethod, + stripe::createPaymentMethod + ) + + @Test + fun `When repository throws exception then createPaymentMethod should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI then throws same exception`( + mockApiRepository::createPaymentMethod, + stripe::createPaymentMethod + ) + + @Test + fun `When repository returns null then createPaymentMethod should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createPaymentMethod, + stripe::createPaymentMethod + ) + + @Test + fun `When repository returns correct value then createSource should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createSource, + stripe::createSource + ) + + @Test + fun `When repository throws exception then createSource should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI then throws same exception`( + mockApiRepository::createSource, + stripe::createSource + ) + + @Test + fun `When repository returns null then createSource should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createSource, + stripe::createSource + ) + + @Test + fun `When repository returns correct value then createAccountToken should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createToken, + stripe::createAccountToken + ) + + @Test + fun `When repository throws exception then createAccountToken should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI then throws same exception`( + mockApiRepository::createToken, + stripe::createAccountToken + ) + + @Test + fun `When repository returns null then createAccountToken should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createToken, + stripe::createAccountToken + ) + + @Test + fun `When repository returns correct value then createBankAccountToken should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createToken, + stripe::createBankAccountToken + ) + + @Test + fun `When repository throws exception then createBankAccountToken should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI then throws same exception`( + mockApiRepository::createToken, + stripe::createBankAccountToken + ) + + @Test + fun `When repository returns null then createBankAccountToken should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createToken, + stripe::createBankAccountToken + ) + + @Test + fun `When repository returns correct value then createPiiToken should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI with String param then returns correct result`( + mockApiRepository::createToken, + stripe::createPiiToken + ) + + @Test + fun `When repository throws exception then createPiiToken should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI with String param then throws same exception`( + mockApiRepository::createToken, + stripe::createPiiToken + ) + + @Test + fun `When repository returns null then createPiiToken should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI with String param then throws APIException`( + mockApiRepository::createToken, + stripe::createPiiToken + ) + + @Test + fun `When repository returns correct value then createCardToken should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createToken, + stripe::createCardToken + ) + + @Test + fun `When repository throws exception then createCardToken should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI then throws same exception`( + mockApiRepository::createToken, + stripe::createCardToken + ) + + @Test + fun `When repository returns null then createCardToken should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createToken, + stripe::createCardToken + ) + + @Test + fun `When repository returns correct value then createCvcUpdateToken should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI with String param then returns correct result`( + mockApiRepository::createToken, + stripe::createCvcUpdateToken + ) + + @Test + fun `When repository throws exception then createCvcUpdateToken should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI with String param then throws same exception`( + mockApiRepository::createToken, + stripe::createCvcUpdateToken + ) + + @Test + fun `When repository returns null then createCvcUpdateToken should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI with String param then throws APIException`( + mockApiRepository::createToken, + stripe::createCvcUpdateToken + ) + + @Test + fun `When repository returns correct value then createPersonToken should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createToken, + stripe::createPersonToken + ) + + @Test + fun `When repository throws exception then createPersonToken should throw same exception`(): Unit = + `Given repository throws exception when calling createAPI then throws same exception`( + mockApiRepository::createToken, + stripe::createPersonToken + ) + + @Test + fun `When repository returns null then createPersonToken should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createToken, + stripe::createPersonToken + ) + + @Test + fun `When repository returns correct value then createFile should Succeed`(): Unit = + `Given repository returns non-empty value when calling createAPI then returns correct result`( + mockApiRepository::createFile, + stripe::createFile + ) + + @Test + fun `When repository returns null then createFile should throw APIException`(): Unit = + `Given repository returns empty value when calling createAPI then thorws APIException`( + mockApiRepository::createFile, + stripe::createFile + ) + + @Test + fun `When repository returns correct value then retrievePaymentIntent should Succeed`(): Unit = + `Given repository returns non-empty value when calling retrieveAPI with String param then returns correct result`( + mockApiRepository::retrievePaymentIntent, + stripe::retrievePaymentIntent + ) + + @Test + fun `When repository throws exception then retrievePaymentIntent should throw same exception`(): Unit = + `Given repository throws exception when calling retrieveAPI with String param then throws same exception`( + mockApiRepository::retrievePaymentIntent, + stripe::retrievePaymentIntent + ) + + @Test + fun `When repository returns null then retrievePaymentIntent should throw APIException`(): Unit = + `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( + mockApiRepository::retrievePaymentIntent, + stripe::retrievePaymentIntent + ) + + @Test + fun `When repository returns correct value then retrieveSetupIntent should Succeed`(): Unit = + `Given repository returns non-empty value when calling retrieveAPI with String param then returns correct result`( + mockApiRepository::retrieveSetupIntent, + stripe::retrieveSetupIntent + ) + + @Test + fun `When repository throws exception then retrieveSetupIntent should throw same exception`(): Unit = + `Given repository throws exception when calling retrieveAPI with String param then throws same exception`( + mockApiRepository::retrieveSetupIntent, + stripe::retrieveSetupIntent + ) + + @Test + fun `When repository returns null then retrieveSetupIntent should throw APIException`(): Unit = + `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( + mockApiRepository::retrieveSetupIntent, + stripe::retrieveSetupIntent + ) + + @Test + fun `When repository returns correct value then retrieveSource should Succeed`() = runBlocking { + val expectedApiObj = mock() + `when`( + mockApiRepository.retrieveSource(any(), any(), any()) + ).thenReturn(expectedApiObj) + val actualObj = stripe.retrieveSource( + "Dummy String param1", + "Dummy String param2", + TEST_STRIPE_ACCOUNT_ID + ) + + assertSame(expectedApiObj, actualObj) + } + + @Test + fun `When repository throws exception then retrieveSource should throw same exception`(): Unit = + runBlocking { + `when`( + mockApiRepository.retrieveSource(any(), any(), any()) + ).thenThrow(mock()) + + assertFailsWith { + stripe.retrieveSource( + "Dummy String param1", + "Dummy String param2", + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + @Test + fun `When repository returns null then retrieveSource should throw APIException`(): Unit = + runBlocking { + `when`( + mockApiRepository.retrieveSource(any(), any(), any()) + ).thenReturn(null) + + assertFailsWith { + stripe.retrieveSource( + "Dummy String param1", + "Dummy String param2", + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + @Test + fun `When repository returns correct value then confirmSetupIntentSuspend should Succeed`() = + runBlocking { + val expectedApiObj = mock() + `when`( + mockApiRepository.confirmSetupIntent(any(), any(), any()) + ).thenReturn(expectedApiObj) + val actualObj = stripe.confirmSetupIntentSuspend(mock()) + + assertSame(expectedApiObj, actualObj) + } + + @Test + fun `When repository throws exception then confirmSetupIntentSuspend should throw same exception`(): Unit = + runBlocking { + `when`( + mockApiRepository.confirmSetupIntent(any(), any(), any()) + ).thenThrow(mock()) + + assertFailsWith { + stripe.confirmSetupIntentSuspend(mock()) + } + } + + @Test + fun `When repository returns null then confirmSetupIntentSuspend should throw APIException`(): Unit = + runBlocking { + `when`( + mockApiRepository.confirmSetupIntent(any(), any(), any()) + ).thenReturn(null) + + assertFailsWith { + stripe.confirmSetupIntentSuspend(mock()) + } + } + + @Test + fun `When repository returns correct value then confirmPaymentIntentSuspend should Succeed`() = + runBlocking { + val expectedApiObj = mock() + `when`( + mockApiRepository.confirmPaymentIntent(any(), any(), any()) + ).thenReturn(expectedApiObj) + val actualObj = stripe.confirmPaymentIntentSuspend(mock()) + + assertSame(expectedApiObj, actualObj) + } + + @Test + fun `When repository throws exception then confirmPaymentIntentSuspend should throw same exception`(): Unit = + runBlocking { + `when`( + mockApiRepository.confirmPaymentIntent(any(), any(), any()) + ).thenThrow(mock()) + + assertFailsWith { + stripe.confirmPaymentIntentSuspend(mock()) + } + } + + @Test + fun `When repository returns null then confirmPaymentIntentSuspend should throw APIException`(): Unit = + runBlocking { + `when`( + mockApiRepository.confirmPaymentIntent(any(), any(), any()) + ).thenReturn(null) + + assertFailsWith { + stripe.confirmPaymentIntentSuspend(mock()) + } + } + + @Test + fun `When controller returns correct value then getPaymentIntentResult should succeed`(): Unit = + `Given controller returns non-empty value when calling getAPI then returns correct result`( + mockPaymentController::shouldHandlePaymentResult, + mockPaymentController::getPaymentIntentResult, + stripe::getPaymentIntentResult + ) + + @Test + fun `When isNotForSetupIntent then getPaymentIntentResult should throw IllegalArgumentException`(): Unit = + `Given controller check fails when calling getAPI then throws IllegalArgumentException`( + mockPaymentController::shouldHandlePaymentResult, + stripe::getPaymentIntentResult + ) + + @Test + fun `When controller throws exception then getPaymentIntentResult should throw same exception`(): Unit = + `Given controller returns exception when calling getAPI then throws same exception`( + mockPaymentController::shouldHandlePaymentResult, + mockPaymentController::getPaymentIntentResult, + stripe::getPaymentIntentResult + ) + + @Test + fun `When controller returns correct value then getSetupIntentResult should succeed`(): Unit = + `Given controller returns non-empty value when calling getAPI then returns correct result`( + mockPaymentController::shouldHandleSetupResult, + mockPaymentController::getSetupIntentResult, + stripe::getSetupIntentResult + ) + + @Test + fun `When isNotForSetupIntent then getSetupIntentResult should throw IllegalArgumentException`(): Unit = + `Given controller check fails when calling getAPI then throws IllegalArgumentException`( + mockPaymentController::shouldHandleSetupResult, + stripe::getSetupIntentResult + ) + + @Test + fun `When controller throws exception then getSetupIntentResult should throw same exception`(): Unit = + `Given controller returns exception when calling getAPI then throws same exception`( + mockPaymentController::shouldHandleSetupResult, + mockPaymentController::getSetupIntentResult, + stripe::getSetupIntentResult + ) + + @Test + fun `When controller returns correct value then getAuthenticateSourceResult should succeed`(): Unit = + `Given controller returns non-empty value when calling getAPI then returns correct result`( + mockPaymentController::shouldHandleSourceResult, + mockPaymentController::getSource, + stripe::getAuthenticateSourceResult + ) + + @Test + fun `When isNotForSetupIntent then getAuthenticateSourceResult should throw IllegalArgumentException`(): Unit = + `Given controller check fails when calling getAPI then throws IllegalArgumentException`( + mockPaymentController::shouldHandleSourceResult, + stripe::getAuthenticateSourceResult + ) + + @Test + fun `When controller throws exception then getAuthenticateSourceResult should throw same exception`(): Unit = + `Given controller returns exception when calling getAPI then throws same exception`( + mockPaymentController::shouldHandleSourceResult, + mockPaymentController::getSource, + stripe::getAuthenticateSourceResult + ) + + private inline fun + `Given repository returns non-empty value when calling createAPI then returns correct result`( + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject + ) = runBlocking { + val expectedApiObj = mock() + + `when`( + repositoryInvocationBlock(any(), any()) + ).thenReturn(expectedApiObj) + + val actualObj = createAPIInvocationBlock( + mock(), + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + + assertSame(expectedApiObj, actualObj) + } + + private inline fun + `Given repository throws exception when calling createAPI then throws same exception`( + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject + ): Unit = runBlocking { + `when`( + repositoryInvocationBlock(any(), any()) + ).thenThrow(mock()) + + assertFailsWith { + createAPIInvocationBlock( + mock(), + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + private inline fun + `Given repository returns empty value when calling createAPI then thorws APIException`( + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject + ): Unit = runBlocking { + `when`( + repositoryInvocationBlock(any(), any()) + ).thenReturn(null) + + assertFailsWith { + createAPIInvocationBlock( + mock(), + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + private inline fun + `Given repository returns non-empty value when calling createAPI with String param then returns correct result`( + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject + ) = runBlocking { + val expectedApiObj = mock() + + `when`( + repositoryInvocationBlock(any(), any()) + ).thenReturn(expectedApiObj) + + val actualObj = createAPIInvocationBlock( + "Dummy String param", + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + + assertSame(expectedApiObj, actualObj) + } + + private inline fun + `Given repository throws exception when calling createAPI with String param then throws same exception`( + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject + ): Unit = runBlocking { + `when`( + repositoryInvocationBlock(any(), any()) + ).thenThrow(mock()) + + assertFailsWith { + createAPIInvocationBlock( + "Dummy String param", + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + private inline fun + `Given repository returns empty value when calling createAPI with String param then throws APIException`( + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject + ): Unit = runBlocking { + `when`( + repositoryInvocationBlock(any(), any()) + ).thenReturn(null) + + assertFailsWith { + createAPIInvocationBlock( + "Dummy String param", + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + private inline fun + `Given repository returns non-empty value when calling retrieveAPI with String param then returns correct result`( + crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, + crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject + ) = runBlocking { + val expectedApiObj = mock() + + `when`( + repositoryInvocationBlock(any(), any(), any()) + ).thenReturn(expectedApiObj) + + val actualObj = retrieveAPIInvocationBlock( + "Dummy String param", + TEST_STRIPE_ACCOUNT_ID + ) + + assertSame(expectedApiObj, actualObj) + } + + private inline fun + `Given repository throws exception when calling retrieveAPI with String param then throws same exception`( + crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, + crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject + ): Unit = runBlocking { + `when`( + repositoryInvocationBlock(any(), any(), any()) + ).thenThrow(mock()) + + assertFailsWith { + retrieveAPIInvocationBlock( + "Dummy String param", + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + private inline fun + `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( + crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, + crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject + ): Unit = runBlocking { + `when`( + repositoryInvocationBlock(any(), any(), any()) + ).thenReturn(null) + + assertFailsWith { + retrieveAPIInvocationBlock( + "Dummy String param", + TEST_STRIPE_ACCOUNT_ID + ) + } + } + + private inline fun + `Given controller returns non-empty value when calling getAPI then returns correct result`( + crossinline controllerCheckBlock: (Int, Intent?) -> Boolean, + crossinline controllerInvocationBlock: suspend (Intent) -> APIObject, + crossinline getAPIInvocationBlock: suspend (Int, Intent?) -> APIObject + ) = runBlocking { + val expectedApiObj = mock() + + `when`( + controllerCheckBlock(any(), any()) + ).thenReturn(true) + + `when`( + controllerInvocationBlock(any()) + ).thenReturn(expectedApiObj) + + val actualObj = getAPIInvocationBlock( + TEST_REQUEST_CODE, + mock() + ) + + assertSame(expectedApiObj, actualObj) + } + + private inline fun + `Given controller check fails when calling getAPI then throws IllegalArgumentException`( + crossinline controllerCheckBlock: (Int, Intent?) -> Boolean, + crossinline getAPIInvocationBlock: suspend (Int, Intent?) -> APIObject + ): Unit = runBlocking { + `when`( + controllerCheckBlock(any(), any()) + ).thenReturn(false) + + assertFailsWith { + getAPIInvocationBlock( + TEST_REQUEST_CODE, + mock() + ) + } + } + + private inline fun + `Given controller returns exception when calling getAPI then throws same exception`( + crossinline controllerCheckBlock: (Int, Intent?) -> Boolean, + crossinline controllerInvocationBlock: suspend (Intent) -> APIObject, + crossinline getAPIInvocationBlock: suspend (Int, Intent?) -> APIObject + ): Unit = runBlocking { + `when`( + controllerCheckBlock(any(), any()) + ).thenReturn(true) + + `when`( + controllerInvocationBlock(any()) + ).thenThrow(mock()) + + assertFailsWith { + getAPIInvocationBlock( + TEST_REQUEST_CODE, + mock() + ) + } + } + + private companion object { + const val TEST_PUBLISHABLE_KEY = "test_publishable_key" + const val TEST_IDEMPOTENCY_KEY = "test_idempotenc_key" + const val TEST_STRIPE_ACCOUNT_ID = "test_account_id" + const val TEST_REQUEST_CODE = 1 + } +} From 18490a7bba71760377e01197f8b749831bad22a9 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Fri, 2 Apr 2021 23:38:14 -0700 Subject: [PATCH 2/6] dump API --- stripe/api/stripe.api | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/stripe/api/stripe.api b/stripe/api/stripe.api index 409463850a6..420637dc1aa 100644 --- a/stripe/api/stripe.api +++ b/stripe/api/stripe.api @@ -821,6 +821,8 @@ public final class com/stripe/android/Stripe { public static synthetic fun handleNextActionForSetupIntent$default (Lcom/stripe/android/Stripe;Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun handleNextActionForSetupIntent$default (Lcom/stripe/android/Stripe;Landroidx/fragment/app/Fragment;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V public final fun isAuthenticateSourceResult (ILandroid/content/Intent;)Z + public final fun isForPaymentIntentResult (ILandroid/content/Intent;)Z + public final fun isForSetupIntentResult (ILandroid/content/Intent;)Z public final fun onAuthenticateSourceResult (Landroid/content/Intent;Lcom/stripe/android/ApiResultCallback;)V public final fun onPaymentResult (ILandroid/content/Intent;Lcom/stripe/android/ApiResultCallback;)Z public final fun onSetupResult (ILandroid/content/Intent;Lcom/stripe/android/ApiResultCallback;)Z @@ -909,6 +911,40 @@ public final class com/stripe/android/StripeIntentResult$Outcome$Companion { public static final field UNKNOWN I } +public final class com/stripe/android/StripeSuspendKt { + public static final fun confirmPaymentIntentSuspend (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun confirmPaymentIntentSuspend$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun confirmSetupIntentSuspend (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun confirmSetupIntentSuspend$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createAccountToken (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/AccountParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createAccountToken$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/AccountParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createBankAccountToken (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/BankAccountTokenParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createBankAccountToken$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/BankAccountTokenParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createCardToken (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/CardParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createCardToken$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/CardParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createCvcUpdateToken (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createCvcUpdateToken$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createFile (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/StripeFileParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createFile$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/StripeFileParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createPaymentMethod (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createPaymentMethod$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/PaymentMethodCreateParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createPersonToken (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/PersonTokenParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createPersonToken$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/PersonTokenParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createPiiToken (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createPiiToken$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun createSource (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createSource$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun getAuthenticateSourceResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getPaymentIntentResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun getSetupIntentResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun retrievePaymentIntent (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun retrievePaymentIntent$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun retrieveSetupIntent (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun retrieveSetupIntent$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun retrieveSource (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun retrieveSource$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + public final class com/stripe/android/StripeTextUtils { public static final field INSTANCE Lcom/stripe/android/StripeTextUtils; public static final fun removeSpacesAndHyphens (Ljava/lang/String;)Ljava/lang/String; From b05744a11b8401bc80bf4e4162f24a5c2e259fc0 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Mon, 5 Apr 2021 00:47:39 -0700 Subject: [PATCH 3/6] resolve comments --- stripe/api/stripe.api | 7 +- .../com/stripe/android/PaymentController.kt | 70 ------- .../main/java/com/stripe/android/Stripe.kt | 42 +--- .../{StripeSuspend.kt => StripeKtx.kt} | 143 ++------------ .../stripe/android/StripePaymentController.kt | 101 ---------- .../com/stripe/android/StripeKotlinTest.kt | 183 +++--------------- 6 files changed, 52 insertions(+), 494 deletions(-) rename stripe/src/main/java/com/stripe/android/{StripeSuspend.kt => StripeKtx.kt} (81%) diff --git a/stripe/api/stripe.api b/stripe/api/stripe.api index 420637dc1aa..949902cef3e 100644 --- a/stripe/api/stripe.api +++ b/stripe/api/stripe.api @@ -821,8 +821,6 @@ public final class com/stripe/android/Stripe { public static synthetic fun handleNextActionForSetupIntent$default (Lcom/stripe/android/Stripe;Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun handleNextActionForSetupIntent$default (Lcom/stripe/android/Stripe;Landroidx/fragment/app/Fragment;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V public final fun isAuthenticateSourceResult (ILandroid/content/Intent;)Z - public final fun isForPaymentIntentResult (ILandroid/content/Intent;)Z - public final fun isForSetupIntentResult (ILandroid/content/Intent;)Z public final fun onAuthenticateSourceResult (Landroid/content/Intent;Lcom/stripe/android/ApiResultCallback;)V public final fun onPaymentResult (ILandroid/content/Intent;Lcom/stripe/android/ApiResultCallback;)Z public final fun onSetupResult (ILandroid/content/Intent;Lcom/stripe/android/ApiResultCallback;)Z @@ -911,7 +909,7 @@ public final class com/stripe/android/StripeIntentResult$Outcome$Companion { public static final field UNKNOWN I } -public final class com/stripe/android/StripeSuspendKt { +public final class com/stripe/android/StripeKtxKt { public static final fun confirmPaymentIntentSuspend (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun confirmPaymentIntentSuspend$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun confirmSetupIntentSuspend (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -934,9 +932,6 @@ public final class com/stripe/android/StripeSuspendKt { public static synthetic fun createPiiToken$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun createSource (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createSource$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/SourceParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun getAuthenticateSourceResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun getPaymentIntentResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun getSetupIntentResult (Lcom/stripe/android/Stripe;ILandroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun retrievePaymentIntent (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun retrievePaymentIntent$default (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun retrieveSetupIntent (Lcom/stripe/android/Stripe;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/stripe/src/main/java/com/stripe/android/PaymentController.kt b/stripe/src/main/java/com/stripe/android/PaymentController.kt index e846c7b34b4..59394abf99f 100644 --- a/stripe/src/main/java/com/stripe/android/PaymentController.kt +++ b/stripe/src/main/java/com/stripe/android/PaymentController.kt @@ -1,10 +1,6 @@ package com.stripe.android import android.content.Intent -import com.stripe.android.exception.APIConnectionException -import com.stripe.android.exception.APIException -import com.stripe.android.exception.AuthenticationException -import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmStripeIntentParams import com.stripe.android.model.Source @@ -68,28 +64,6 @@ internal interface PaymentController { callback: ApiResultCallback ) - /** - * Get the PaymentIntent's client_secret from {@param data} and use to retrieve - * the PaymentIntent object with updated status. - * - * @param data the result Intent - * @return the [PaymentIntentResult] object - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - * @throws IllegalArgumentException if the PaymentIntent response's JsonParser returns null - */ - @Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class - ) - suspend fun getPaymentIntentResult(data: Intent): PaymentIntentResult - /** * If setup authentication triggered an exception, get the exception object and pass to * [ApiResultCallback.onError]. @@ -104,55 +78,11 @@ internal interface PaymentController { callback: ApiResultCallback ) - /** - * Get the SetupIntent's client_secret from {@param data} and use to retrieve - * the SetupIntent object with updated status. - * - * @param data the result Intent - * @return the [SetupIntentResult] object - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - * @throws IllegalArgumentException if the SetupIntent response's JsonParser returns null - */ - @Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class - ) - suspend fun getSetupIntentResult(data: Intent): SetupIntentResult - fun handleSourceResult( data: Intent, callback: ApiResultCallback ) - /** - * Get the Source's client_secret from {@param data} and use to retrieve - * the Source object with updated status. - * - * @param data the result Intent - * @return the [Source] object - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - * @throws IllegalArgumentException if the Source response's JsonParser returns null - */ - @Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class - ) - suspend fun getSource(data: Intent): Source - /** * Determine which authentication mechanism should be used, or bypass authentication * if it is not needed. diff --git a/stripe/src/main/java/com/stripe/android/Stripe.kt b/stripe/src/main/java/com/stripe/android/Stripe.kt index 82a997f4c01..9b3f826974f 100644 --- a/stripe/src/main/java/com/stripe/android/Stripe.kt +++ b/stripe/src/main/java/com/stripe/android/Stripe.kt @@ -322,20 +322,6 @@ class Stripe internal constructor( ) } - /** - * Check if the requestCode and intent is for [PaymentIntentResult]. - * The Intent should the retrieved from result from `Activity#onActivityResult(int, int, Intent)}}` - * by activity started with [confirmPayment] or [handleNextActionForPayment]. - * - * @return whether the requestCode and intent is for [PaymentIntentResult]. - */ - fun isForPaymentIntentResult( - requestCode: Int, - data: Intent? - ): Boolean { - return data != null && paymentController.shouldHandlePaymentResult(requestCode, data) - } - /** * Should be called via `Activity#onActivityResult(int, int, Intent)}}` to handle the * result of a PaymentIntent automatic confirmation (see [confirmPayment]) or @@ -347,8 +333,8 @@ class Stripe internal constructor( data: Intent?, callback: ApiResultCallback ): Boolean { - return if (isForPaymentIntentResult(requestCode, data)) { - paymentController.handlePaymentResult(data!!, callback) + return if (data != null && paymentController.shouldHandlePaymentResult(requestCode, data)) { + paymentController.handlePaymentResult(data, callback) true } else { false @@ -611,20 +597,6 @@ class Stripe internal constructor( ) } - /** - * Check if the requestCode and intent is for [SetupIntentResult]. - * The Intent should the retrieved from result from `Activity#onActivityResult(int, int, Intent)}}` - * by activity started with [confirmSetupIntent]. - * - * @return whether the requestCode and intent is for [SetupIntentResult]. - */ - fun isForSetupIntentResult( - requestCode: Int, - data: Intent? - ): Boolean { - return data != null && paymentController.shouldHandleSetupResult(requestCode, data) - } - /** * Should be called via `Activity#onActivityResult(int, int, Intent)}}` to handle the * result of a SetupIntent confirmation (see [confirmSetupIntent]). @@ -635,8 +607,8 @@ class Stripe internal constructor( data: Intent?, callback: ApiResultCallback ): Boolean { - return if (isForSetupIntentResult(requestCode, data)) { - paymentController.handleSetupResult(data!!, callback) + return if (data != null && paymentController.shouldHandleSetupResult(requestCode, data)) { + paymentController.handleSetupResult(data, callback) true } else { false @@ -878,11 +850,7 @@ class Stripe internal constructor( } /** - * Check if the requestCode and intent is for [Source] authentication. - * The Intent should the retrieved from result from `Activity#onActivityResult(int, int, Intent)}}` - * by activity started with [authenticateSource]. - * - * @return whether the requestCode and intent is for [Source] authentication + * Should be called in `onActivityResult()` to determine if the result is for Source authentication */ fun isAuthenticateSourceResult( requestCode: Int, diff --git a/stripe/src/main/java/com/stripe/android/StripeSuspend.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt similarity index 81% rename from stripe/src/main/java/com/stripe/android/StripeSuspend.kt rename to stripe/src/main/java/com/stripe/android/StripeKtx.kt index d2ede78dc83..d8c90f858c2 100644 --- a/stripe/src/main/java/com/stripe/android/StripeSuspend.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -1,12 +1,12 @@ package com.stripe.android -import android.content.Intent import androidx.annotation.Size import com.stripe.android.exception.APIConnectionException import com.stripe.android.exception.APIException import com.stripe.android.exception.AuthenticationException import com.stripe.android.exception.CardException import com.stripe.android.exception.InvalidRequestException +import com.stripe.android.exception.StripeException import com.stripe.android.model.AccountParams import com.stripe.android.model.BankAccountTokenParams import com.stripe.android.model.CardParams @@ -56,7 +56,7 @@ suspend fun Stripe.createPaymentMethod( paymentMethodCreateParams: PaymentMethodCreateParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): PaymentMethod = runOnIOAndThrowIfNull { stripeRepository.createPaymentMethod( paymentMethodCreateParams, ApiRequest.Options( @@ -95,7 +95,7 @@ suspend fun Stripe.createSource( sourceParams: SourceParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Source = runOnIOAndThrowIfNull { stripeRepository.createSource( sourceParams, ApiRequest.Options( @@ -134,7 +134,7 @@ suspend fun Stripe.createAccountToken( accountParams: AccountParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Token = runOnIOAndThrowIfNull { stripeRepository.createToken( accountParams, ApiRequest.Options( @@ -173,7 +173,7 @@ suspend fun Stripe.createBankAccountToken( bankAccountTokenParams: BankAccountTokenParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Token = runOnIOAndThrowIfNull { stripeRepository.createToken( bankAccountTokenParams, ApiRequest.Options( @@ -212,7 +212,7 @@ suspend fun Stripe.createPiiToken( personalId: String, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Token = runOnIOAndThrowIfNull { stripeRepository.createToken( PiiTokenParams(personalId), ApiRequest.Options( @@ -253,7 +253,7 @@ suspend fun Stripe.createCardToken( cardParams: CardParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Token = runOnIOAndThrowIfNull { stripeRepository.createToken( cardParams, ApiRequest.Options( @@ -291,7 +291,7 @@ suspend fun Stripe.createCvcUpdateToken( @Size(min = 3, max = 4) cvc: String, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Token = runOnIOAndThrowIfNull { stripeRepository.createToken( CvcTokenParams(cvc), ApiRequest.Options( @@ -331,7 +331,7 @@ suspend fun Stripe.createPersonToken( params: PersonTokenParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Token = runOnIOAndThrowIfNull { stripeRepository.createToken( params, ApiRequest.Options( @@ -372,7 +372,7 @@ suspend fun Stripe.createFile( fileParams: StripeFileParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId, -) = runOnIOAndThrowIfNull { +): StripeFile = runOnIOAndThrowIfNull { stripeRepository.createFile( fileParams, ApiRequest.Options( @@ -409,7 +409,7 @@ suspend fun Stripe.createFile( suspend fun Stripe.retrievePaymentIntent( clientSecret: String, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): PaymentIntent = runOnIOAndThrowIfNull { stripeRepository.retrievePaymentIntent( clientSecret, ApiRequest.Options( @@ -445,7 +445,7 @@ suspend fun Stripe.retrievePaymentIntent( suspend fun Stripe.retrieveSetupIntent( clientSecret: String, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): SetupIntent = runOnIOAndThrowIfNull { stripeRepository.retrieveSetupIntent( clientSecret, ApiRequest.Options( @@ -483,7 +483,7 @@ suspend fun Stripe.retrieveSource( @Size(min = 1) sourceId: String, @Size(min = 1) clientSecret: String, stripeAccountId: String? = this.stripeAccountId -) = runOnIOAndThrowIfNull { +): Source = runOnIOAndThrowIfNull { stripeRepository.retrieveSource( sourceId, clientSecret, @@ -519,7 +519,7 @@ suspend fun Stripe.retrieveSource( suspend fun Stripe.confirmSetupIntentSuspend( confirmSetupIntentParams: ConfirmSetupIntentParams, idempotencyKey: String? = null -) = runOnIOAndThrowIfNull { +): SetupIntent = runOnIOAndThrowIfNull { stripeRepository.confirmSetupIntent( confirmSetupIntentParams, ApiRequest.Options( @@ -555,7 +555,7 @@ suspend fun Stripe.confirmSetupIntentSuspend( suspend fun Stripe.confirmPaymentIntentSuspend( confirmPaymentIntentParams: ConfirmPaymentIntentParams, idempotencyKey: String? = null -) = runOnIOAndThrowIfNull { +): PaymentIntent = runOnIOAndThrowIfNull { stripeRepository.confirmPaymentIntent( confirmPaymentIntentParams, ApiRequest.Options( @@ -567,116 +567,15 @@ suspend fun Stripe.confirmPaymentIntentSuspend( } /** - * Consume the empty result from Stripe's internal Json Parser, throw [APIException] for public API. + * Consume the empty result from Stripe's internal Json Parser, throw [InvalidRequestException] for public API. */ internal suspend inline fun runOnIOAndThrowIfNull( crossinline block: suspend () -> T? ): T = withContext(Dispatchers.IO) { - block() ?: run { - throw APIException(message = "${T::class.java.simpleName}'s JsonParser returns null.") - } - } - -/** - * Get the [PaymentIntentResult] from [Intent] returned via - * Activity#onActivityResult(int, int, Intent)}} for PaymentIntent automatic confirmation - * (see [confirmPayment]) or manual confirmation (see [handleNextActionForPayment]}) - * - * @param requestCode [Int] code passed from Activity#onActivityResult - * @param data [Intent] intent from Activity#onActivityResult - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - */ -@Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, -) -suspend fun Stripe.getPaymentIntentResult( - requestCode: Int, - data: Intent?, -): PaymentIntentResult { - return if ( - isForPaymentIntentResult(requestCode, data) - ) runOnIOAndThrowForIllegalArgumentException { paymentController.getPaymentIntentResult(data!!) } - else throw IllegalArgumentException("Intent is not from Stripe#confirmPayment or Stripe#handleNextActionForPayment.") -} - -/** - * Get the [SetupIntentResult] from [Intent] returned via - * Activity#onActivityResult(int, int, Intent)}} for SetupIntentResult confirmation. - * (see [confirmSetupIntent]) - * - * @param requestCode [Int] code passed from Activity#onActivityResult - * @param data [Intent] intent from Activity#onActivityResult - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - */ -@Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class -) -suspend fun Stripe.getSetupIntentResult( - requestCode: Int, - data: Intent?, -): SetupIntentResult { - return if ( - isForSetupIntentResult(requestCode, data) - ) runOnIOAndThrowForIllegalArgumentException { paymentController.getSetupIntentResult(data!!) } - else throw IllegalArgumentException("Intent is not from Stripe#confirmSetupIntent.") -} - -/** - * Get the [Source] from [Intent] returned via - * Activity#onActivityResult(int, int, Intent)}} for [Source] authentication. - * (see [authenticateSource]) - * - * @param requestCode [Int] code passed from Activity#onActivityResult - * @param data [Intent] intent from Activity#onActivityResult - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - */ -@Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class -) -suspend fun Stripe.getAuthenticateSourceResult( - requestCode: Int, - data: Intent?, -): Source { - return if ( - isAuthenticateSourceResult(requestCode, data) - ) runOnIOAndThrowForIllegalArgumentException { paymentController.getSource(data!!) } - else throw IllegalArgumentException("Intent is not from Stripe#authenticateSource.") -} - -/** - * Consume the [IllegalArgumentException] caused by empty result from Stripe's internal Json Parser, - * throw [APIException] for public API. - */ -internal suspend inline fun runOnIOAndThrowForIllegalArgumentException( - crossinline block: suspend () -> T -): T = - withContext(Dispatchers.IO) { - try { - block() - } catch (e: IllegalArgumentException) { - throw APIException(message = "${T::class.java.simpleName}'s JsonParser returns null.") - } + runCatching { + requireNotNull(block()) { + "Failed to parse ${T::class.java.simpleName}." + } + }.getOrElse { throw StripeException.create(it) } } diff --git a/stripe/src/main/java/com/stripe/android/StripePaymentController.kt b/stripe/src/main/java/com/stripe/android/StripePaymentController.kt index fcff6c47f5f..3c66cf7048a 100644 --- a/stripe/src/main/java/com/stripe/android/StripePaymentController.kt +++ b/stripe/src/main/java/com/stripe/android/StripePaymentController.kt @@ -5,10 +5,6 @@ import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.annotation.VisibleForTesting import com.stripe.android.auth.PaymentAuthWebViewContract -import com.stripe.android.exception.APIConnectionException -import com.stripe.android.exception.APIException -import com.stripe.android.exception.AuthenticationException -import com.stripe.android.exception.InvalidRequestException import com.stripe.android.exception.StripeException import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams @@ -396,31 +392,6 @@ internal class StripePaymentController internal constructor( } } - /** - * Get the PaymentIntent's client_secret from {@param data} and use to retrieve - * the PaymentIntent object with updated status. - * - * @param data the result Intent - * @return the [PaymentIntentResult] object - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - * @throws IllegalArgumentException if the response's JsonParser returns null - */ - @Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class - ) - override suspend fun getPaymentIntentResult(data: Intent) = - paymentFlowResultProcessor.processPaymentIntent( - PaymentFlowResult.Unvalidated.fromIntent(data) - ) - /** * If setup authentication triggered an exception, get the exception object and pass to * [ApiResultCallback.onError]. @@ -450,31 +421,6 @@ internal class StripePaymentController internal constructor( } } - /** - * Get the SetupIntent's client_secret from {@param data} and use to retrieve - * the PaymentIntent object with updated status. - * - * @param data the result Intent - * @return the [SetupIntentResult] object - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - * @throws IllegalArgumentException if the response's JsonParser returns null - */ - @Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class - ) - override suspend fun getSetupIntentResult(data: Intent) = - paymentFlowResultProcessor.processSetupIntent( - PaymentFlowResult.Unvalidated.fromIntent(data) - ) - override fun handleSourceResult( data: Intent, callback: ApiResultCallback @@ -515,53 +461,6 @@ internal class StripePaymentController internal constructor( } } - /** - * Get the Source's client_secret from {@param data} and use to retrieve - * the Source object with updated status. - * - * @param data the result Intent - * @return the [Source] object - * - * @throws AuthenticationException failure to properly authenticate yourself (check your key) - * @throws InvalidRequestException your request has invalid parameters - * @throws APIConnectionException failure to connect to Stripe's API - * @throws APIException any other type of problem (for instance, a temporary issue with Stripe's servers) - * @throws IllegalArgumentException if the Source response's JsonParser returns null - */ - @Throws( - AuthenticationException::class, - InvalidRequestException::class, - APIConnectionException::class, - APIException::class, - IllegalArgumentException::class - ) - override suspend fun getSource(data: Intent): Source { - val result = PaymentFlowResult.Unvalidated.fromIntent(data) - val sourceId = result.sourceId.orEmpty() - val clientSecret = result.clientSecret.orEmpty() - - val requestOptions = ApiRequest.Options( - apiKey = publishableKey, - stripeAccount = result.stripeAccountId - ) - - analyticsRequestExecutor.executeAsync( - analyticsRequestFactory.create( - analyticsDataFactory.createAuthSourceParams( - AnalyticsEvent.AuthSourceResult, - sourceId - ) - ) - ) - return requireNotNull( - stripeRepository.retrieveSource( - sourceId, - clientSecret, - requestOptions - ) - ) - } - @VisibleForTesting internal suspend fun authenticateAlipay( paymentIntent: PaymentIntent, diff --git a/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt b/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt index 7470481ccf9..f688e138dc5 100644 --- a/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt +++ b/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt @@ -1,10 +1,11 @@ package com.stripe.android -import android.content.Intent import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever import com.stripe.android.exception.APIException import com.stripe.android.exception.AuthenticationException +import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.model.Source @@ -14,7 +15,6 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import org.mockito.Mockito.`when` import kotlin.test.assertFailsWith import kotlin.test.assertSame @@ -256,7 +256,7 @@ internal class StripeKotlinTest { @Test fun `When repository returns correct value then retrieveSource should Succeed`() = runBlocking { val expectedApiObj = mock() - `when`( + whenever( mockApiRepository.retrieveSource(any(), any(), any()) ).thenReturn(expectedApiObj) val actualObj = stripe.retrieveSource( @@ -271,7 +271,7 @@ internal class StripeKotlinTest { @Test fun `When repository throws exception then retrieveSource should throw same exception`(): Unit = runBlocking { - `when`( + whenever( mockApiRepository.retrieveSource(any(), any(), any()) ).thenThrow(mock()) @@ -287,11 +287,11 @@ internal class StripeKotlinTest { @Test fun `When repository returns null then retrieveSource should throw APIException`(): Unit = runBlocking { - `when`( + whenever( mockApiRepository.retrieveSource(any(), any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWith { stripe.retrieveSource( "Dummy String param1", "Dummy String param2", @@ -304,7 +304,7 @@ internal class StripeKotlinTest { fun `When repository returns correct value then confirmSetupIntentSuspend should Succeed`() = runBlocking { val expectedApiObj = mock() - `when`( + whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenReturn(expectedApiObj) val actualObj = stripe.confirmSetupIntentSuspend(mock()) @@ -315,7 +315,7 @@ internal class StripeKotlinTest { @Test fun `When repository throws exception then confirmSetupIntentSuspend should throw same exception`(): Unit = runBlocking { - `when`( + whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenThrow(mock()) @@ -327,7 +327,7 @@ internal class StripeKotlinTest { @Test fun `When repository returns null then confirmSetupIntentSuspend should throw APIException`(): Unit = runBlocking { - `when`( + whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenReturn(null) @@ -340,7 +340,7 @@ internal class StripeKotlinTest { fun `When repository returns correct value then confirmPaymentIntentSuspend should Succeed`() = runBlocking { val expectedApiObj = mock() - `when`( + whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenReturn(expectedApiObj) val actualObj = stripe.confirmPaymentIntentSuspend(mock()) @@ -351,7 +351,7 @@ internal class StripeKotlinTest { @Test fun `When repository throws exception then confirmPaymentIntentSuspend should throw same exception`(): Unit = runBlocking { - `when`( + whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenThrow(mock()) @@ -363,84 +363,15 @@ internal class StripeKotlinTest { @Test fun `When repository returns null then confirmPaymentIntentSuspend should throw APIException`(): Unit = runBlocking { - `when`( + whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWith { stripe.confirmPaymentIntentSuspend(mock()) } } - @Test - fun `When controller returns correct value then getPaymentIntentResult should succeed`(): Unit = - `Given controller returns non-empty value when calling getAPI then returns correct result`( - mockPaymentController::shouldHandlePaymentResult, - mockPaymentController::getPaymentIntentResult, - stripe::getPaymentIntentResult - ) - - @Test - fun `When isNotForSetupIntent then getPaymentIntentResult should throw IllegalArgumentException`(): Unit = - `Given controller check fails when calling getAPI then throws IllegalArgumentException`( - mockPaymentController::shouldHandlePaymentResult, - stripe::getPaymentIntentResult - ) - - @Test - fun `When controller throws exception then getPaymentIntentResult should throw same exception`(): Unit = - `Given controller returns exception when calling getAPI then throws same exception`( - mockPaymentController::shouldHandlePaymentResult, - mockPaymentController::getPaymentIntentResult, - stripe::getPaymentIntentResult - ) - - @Test - fun `When controller returns correct value then getSetupIntentResult should succeed`(): Unit = - `Given controller returns non-empty value when calling getAPI then returns correct result`( - mockPaymentController::shouldHandleSetupResult, - mockPaymentController::getSetupIntentResult, - stripe::getSetupIntentResult - ) - - @Test - fun `When isNotForSetupIntent then getSetupIntentResult should throw IllegalArgumentException`(): Unit = - `Given controller check fails when calling getAPI then throws IllegalArgumentException`( - mockPaymentController::shouldHandleSetupResult, - stripe::getSetupIntentResult - ) - - @Test - fun `When controller throws exception then getSetupIntentResult should throw same exception`(): Unit = - `Given controller returns exception when calling getAPI then throws same exception`( - mockPaymentController::shouldHandleSetupResult, - mockPaymentController::getSetupIntentResult, - stripe::getSetupIntentResult - ) - - @Test - fun `When controller returns correct value then getAuthenticateSourceResult should succeed`(): Unit = - `Given controller returns non-empty value when calling getAPI then returns correct result`( - mockPaymentController::shouldHandleSourceResult, - mockPaymentController::getSource, - stripe::getAuthenticateSourceResult - ) - - @Test - fun `When isNotForSetupIntent then getAuthenticateSourceResult should throw IllegalArgumentException`(): Unit = - `Given controller check fails when calling getAPI then throws IllegalArgumentException`( - mockPaymentController::shouldHandleSourceResult, - stripe::getAuthenticateSourceResult - ) - - @Test - fun `When controller throws exception then getAuthenticateSourceResult should throw same exception`(): Unit = - `Given controller returns exception when calling getAPI then throws same exception`( - mockPaymentController::shouldHandleSourceResult, - mockPaymentController::getSource, - stripe::getAuthenticateSourceResult - ) - private inline fun `Given repository returns non-empty value when calling createAPI then returns correct result`( crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, @@ -448,7 +379,7 @@ internal class StripeKotlinTest { ) = runBlocking { val expectedApiObj = mock() - `when`( + whenever( repositoryInvocationBlock(any(), any()) ).thenReturn(expectedApiObj) @@ -466,7 +397,7 @@ internal class StripeKotlinTest { crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject ): Unit = runBlocking { - `when`( + whenever( repositoryInvocationBlock(any(), any()) ).thenThrow(mock()) @@ -484,11 +415,11 @@ internal class StripeKotlinTest { crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject ): Unit = runBlocking { - `when`( + whenever( repositoryInvocationBlock(any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWith { createAPIInvocationBlock( mock(), TEST_IDEMPOTENCY_KEY, @@ -504,7 +435,7 @@ internal class StripeKotlinTest { ) = runBlocking { val expectedApiObj = mock() - `when`( + whenever( repositoryInvocationBlock(any(), any()) ).thenReturn(expectedApiObj) @@ -522,7 +453,7 @@ internal class StripeKotlinTest { crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject ): Unit = runBlocking { - `when`( + whenever( repositoryInvocationBlock(any(), any()) ).thenThrow(mock()) @@ -540,11 +471,11 @@ internal class StripeKotlinTest { crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject ): Unit = runBlocking { - `when`( + whenever( repositoryInvocationBlock(any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWith { createAPIInvocationBlock( "Dummy String param", TEST_IDEMPOTENCY_KEY, @@ -560,7 +491,7 @@ internal class StripeKotlinTest { ) = runBlocking { val expectedApiObj = mock() - `when`( + whenever( repositoryInvocationBlock(any(), any(), any()) ).thenReturn(expectedApiObj) @@ -577,7 +508,7 @@ internal class StripeKotlinTest { crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject ): Unit = runBlocking { - `when`( + whenever( repositoryInvocationBlock(any(), any(), any()) ).thenThrow(mock()) @@ -594,11 +525,11 @@ internal class StripeKotlinTest { crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject ): Unit = runBlocking { - `when`( + whenever( repositoryInvocationBlock(any(), any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWith { retrieveAPIInvocationBlock( "Dummy String param", TEST_STRIPE_ACCOUNT_ID @@ -606,73 +537,9 @@ internal class StripeKotlinTest { } } - private inline fun - `Given controller returns non-empty value when calling getAPI then returns correct result`( - crossinline controllerCheckBlock: (Int, Intent?) -> Boolean, - crossinline controllerInvocationBlock: suspend (Intent) -> APIObject, - crossinline getAPIInvocationBlock: suspend (Int, Intent?) -> APIObject - ) = runBlocking { - val expectedApiObj = mock() - - `when`( - controllerCheckBlock(any(), any()) - ).thenReturn(true) - - `when`( - controllerInvocationBlock(any()) - ).thenReturn(expectedApiObj) - - val actualObj = getAPIInvocationBlock( - TEST_REQUEST_CODE, - mock() - ) - - assertSame(expectedApiObj, actualObj) - } - - private inline fun - `Given controller check fails when calling getAPI then throws IllegalArgumentException`( - crossinline controllerCheckBlock: (Int, Intent?) -> Boolean, - crossinline getAPIInvocationBlock: suspend (Int, Intent?) -> APIObject - ): Unit = runBlocking { - `when`( - controllerCheckBlock(any(), any()) - ).thenReturn(false) - - assertFailsWith { - getAPIInvocationBlock( - TEST_REQUEST_CODE, - mock() - ) - } - } - - private inline fun - `Given controller returns exception when calling getAPI then throws same exception`( - crossinline controllerCheckBlock: (Int, Intent?) -> Boolean, - crossinline controllerInvocationBlock: suspend (Intent) -> APIObject, - crossinline getAPIInvocationBlock: suspend (Int, Intent?) -> APIObject - ): Unit = runBlocking { - `when`( - controllerCheckBlock(any(), any()) - ).thenReturn(true) - - `when`( - controllerInvocationBlock(any()) - ).thenThrow(mock()) - - assertFailsWith { - getAPIInvocationBlock( - TEST_REQUEST_CODE, - mock() - ) - } - } - private companion object { const val TEST_PUBLISHABLE_KEY = "test_publishable_key" const val TEST_IDEMPOTENCY_KEY = "test_idempotenc_key" const val TEST_STRIPE_ACCOUNT_ID = "test_account_id" - const val TEST_REQUEST_CODE = 1 } } From dbc5e8a8d0b2700da6f539634304dfdf141d4345 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Mon, 5 Apr 2021 13:07:44 -0700 Subject: [PATCH 4/6] resolve comments --- stripe/api/stripe.api | 8 ++-- .../main/java/com/stripe/android/StripeKtx.kt | 48 +++++++++---------- .../{StripeKotlinTest.kt => StripeKtxTest.kt} | 29 ++++++----- 3 files changed, 44 insertions(+), 41 deletions(-) rename stripe/src/test/java/com/stripe/android/{StripeKotlinTest.kt => StripeKtxTest.kt} (96%) diff --git a/stripe/api/stripe.api b/stripe/api/stripe.api index 949902cef3e..6b9a014dbf9 100644 --- a/stripe/api/stripe.api +++ b/stripe/api/stripe.api @@ -910,10 +910,10 @@ public final class com/stripe/android/StripeIntentResult$Outcome$Companion { } public final class com/stripe/android/StripeKtxKt { - public static final fun confirmPaymentIntentSuspend (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun confirmPaymentIntentSuspend$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun confirmSetupIntentSuspend (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun confirmSetupIntentSuspend$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun confirmPaymentIntent (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun confirmPaymentIntent$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmPaymentIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun confirmSetupIntent (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun confirmSetupIntent$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/ConfirmSetupIntentParams;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun createAccountToken (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/AccountParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createAccountToken$default (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/AccountParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun createBankAccountToken (Lcom/stripe/android/Stripe;Lcom/stripe/android/model/BankAccountTokenParams;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/stripe/src/main/java/com/stripe/android/StripeKtx.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt index d8c90f858c2..3a1528413c3 100644 --- a/stripe/src/main/java/com/stripe/android/StripeKtx.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -25,8 +25,6 @@ import com.stripe.android.model.StripeFile import com.stripe.android.model.StripeFileParams import com.stripe.android.model.Token import com.stripe.android.networking.ApiRequest -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext /** * Create a [PaymentMethod] from a coroutine. @@ -56,7 +54,7 @@ suspend fun Stripe.createPaymentMethod( paymentMethodCreateParams: PaymentMethodCreateParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): PaymentMethod = runOnIOAndThrowIfNull { +): PaymentMethod = runApiRequest { stripeRepository.createPaymentMethod( paymentMethodCreateParams, ApiRequest.Options( @@ -95,7 +93,7 @@ suspend fun Stripe.createSource( sourceParams: SourceParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Source = runOnIOAndThrowIfNull { +): Source = runApiRequest { stripeRepository.createSource( sourceParams, ApiRequest.Options( @@ -134,7 +132,7 @@ suspend fun Stripe.createAccountToken( accountParams: AccountParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Token = runOnIOAndThrowIfNull { +): Token = runApiRequest { stripeRepository.createToken( accountParams, ApiRequest.Options( @@ -173,7 +171,7 @@ suspend fun Stripe.createBankAccountToken( bankAccountTokenParams: BankAccountTokenParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Token = runOnIOAndThrowIfNull { +): Token = runApiRequest { stripeRepository.createToken( bankAccountTokenParams, ApiRequest.Options( @@ -212,7 +210,7 @@ suspend fun Stripe.createPiiToken( personalId: String, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Token = runOnIOAndThrowIfNull { +): Token = runApiRequest { stripeRepository.createToken( PiiTokenParams(personalId), ApiRequest.Options( @@ -253,7 +251,7 @@ suspend fun Stripe.createCardToken( cardParams: CardParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Token = runOnIOAndThrowIfNull { +): Token = runApiRequest { stripeRepository.createToken( cardParams, ApiRequest.Options( @@ -291,7 +289,7 @@ suspend fun Stripe.createCvcUpdateToken( @Size(min = 3, max = 4) cvc: String, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Token = runOnIOAndThrowIfNull { +): Token = runApiRequest { stripeRepository.createToken( CvcTokenParams(cvc), ApiRequest.Options( @@ -331,7 +329,7 @@ suspend fun Stripe.createPersonToken( params: PersonTokenParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId -): Token = runOnIOAndThrowIfNull { +): Token = runApiRequest { stripeRepository.createToken( params, ApiRequest.Options( @@ -372,7 +370,7 @@ suspend fun Stripe.createFile( fileParams: StripeFileParams, idempotencyKey: String? = null, stripeAccountId: String? = this.stripeAccountId, -): StripeFile = runOnIOAndThrowIfNull { +): StripeFile = runApiRequest { stripeRepository.createFile( fileParams, ApiRequest.Options( @@ -409,7 +407,7 @@ suspend fun Stripe.createFile( suspend fun Stripe.retrievePaymentIntent( clientSecret: String, stripeAccountId: String? = this.stripeAccountId -): PaymentIntent = runOnIOAndThrowIfNull { +): PaymentIntent = runApiRequest { stripeRepository.retrievePaymentIntent( clientSecret, ApiRequest.Options( @@ -445,7 +443,7 @@ suspend fun Stripe.retrievePaymentIntent( suspend fun Stripe.retrieveSetupIntent( clientSecret: String, stripeAccountId: String? = this.stripeAccountId -): SetupIntent = runOnIOAndThrowIfNull { +): SetupIntent = runApiRequest { stripeRepository.retrieveSetupIntent( clientSecret, ApiRequest.Options( @@ -483,7 +481,7 @@ suspend fun Stripe.retrieveSource( @Size(min = 1) sourceId: String, @Size(min = 1) clientSecret: String, stripeAccountId: String? = this.stripeAccountId -): Source = runOnIOAndThrowIfNull { +): Source = runApiRequest { stripeRepository.retrieveSource( sourceId, clientSecret, @@ -516,10 +514,10 @@ suspend fun Stripe.retrieveSource( APIConnectionException::class, APIException::class ) -suspend fun Stripe.confirmSetupIntentSuspend( +suspend fun Stripe.confirmSetupIntent( confirmSetupIntentParams: ConfirmSetupIntentParams, idempotencyKey: String? = null -): SetupIntent = runOnIOAndThrowIfNull { +): SetupIntent = runApiRequest { stripeRepository.confirmSetupIntent( confirmSetupIntentParams, ApiRequest.Options( @@ -552,10 +550,10 @@ suspend fun Stripe.confirmSetupIntentSuspend( APIConnectionException::class, APIException::class ) -suspend fun Stripe.confirmPaymentIntentSuspend( +suspend fun Stripe.confirmPaymentIntent( confirmPaymentIntentParams: ConfirmPaymentIntentParams, idempotencyKey: String? = null -): PaymentIntent = runOnIOAndThrowIfNull { +): PaymentIntent = runApiRequest { stripeRepository.confirmPaymentIntent( confirmPaymentIntentParams, ApiRequest.Options( @@ -569,13 +567,11 @@ suspend fun Stripe.confirmPaymentIntentSuspend( /** * Consume the empty result from Stripe's internal Json Parser, throw [InvalidRequestException] for public API. */ -internal suspend inline fun runOnIOAndThrowIfNull( +private suspend inline fun runApiRequest( crossinline block: suspend () -> T? ): T = - withContext(Dispatchers.IO) { - runCatching { - requireNotNull(block()) { - "Failed to parse ${T::class.java.simpleName}." - } - }.getOrElse { throw StripeException.create(it) } - } + runCatching { + requireNotNull(block()) { + "Failed to parse ${T::class.java.simpleName}." + } + }.getOrElse { throw StripeException.create(it) } diff --git a/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt b/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt similarity index 96% rename from stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt rename to stripe/src/test/java/com/stripe/android/StripeKtxTest.kt index f688e138dc5..846b734a963 100644 --- a/stripe/src/test/java/com/stripe/android/StripeKotlinTest.kt +++ b/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt @@ -3,7 +3,6 @@ package com.stripe.android import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever -import com.stripe.android.exception.APIException import com.stripe.android.exception.AuthenticationException import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.PaymentIntent @@ -11,7 +10,9 @@ import com.stripe.android.model.SetupIntent import com.stripe.android.model.Source import com.stripe.android.networking.ApiRequest import com.stripe.android.networking.StripeApiRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -21,13 +22,20 @@ import kotlin.test.assertSame /** * Test for [Stripe] suspend functions. */ +@ExperimentalCoroutinesApi @RunWith(JUnit4::class) -internal class StripeKotlinTest { +internal class StripeKtxTest { private val mockApiRepository: StripeApiRepository = mock() private val mockPaymentController: PaymentController = mock() private val stripe: Stripe = - Stripe(mockApiRepository, mockPaymentController, TEST_PUBLISHABLE_KEY) + Stripe( + mockApiRepository, + mockPaymentController, + ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, + TEST_STRIPE_ACCOUNT_ID, + TestCoroutineDispatcher() + ) @Test fun `When repository returns correct value then createPaymentMethod should Succeed`(): Unit = @@ -307,7 +315,7 @@ internal class StripeKotlinTest { whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenReturn(expectedApiObj) - val actualObj = stripe.confirmSetupIntentSuspend(mock()) + val actualObj = stripe.confirmSetupIntent(mock()) assertSame(expectedApiObj, actualObj) } @@ -320,7 +328,7 @@ internal class StripeKotlinTest { ).thenThrow(mock()) assertFailsWith { - stripe.confirmSetupIntentSuspend(mock()) + stripe.confirmSetupIntent(mock()) } } @@ -331,8 +339,8 @@ internal class StripeKotlinTest { mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenReturn(null) - assertFailsWith { - stripe.confirmSetupIntentSuspend(mock()) + assertFailsWith { + stripe.confirmSetupIntent(mock()) } } @@ -343,7 +351,7 @@ internal class StripeKotlinTest { whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenReturn(expectedApiObj) - val actualObj = stripe.confirmPaymentIntentSuspend(mock()) + val actualObj = stripe.confirmPaymentIntent(mock()) assertSame(expectedApiObj, actualObj) } @@ -356,7 +364,7 @@ internal class StripeKotlinTest { ).thenThrow(mock()) assertFailsWith { - stripe.confirmPaymentIntentSuspend(mock()) + stripe.confirmPaymentIntent(mock()) } } @@ -368,7 +376,7 @@ internal class StripeKotlinTest { ).thenReturn(null) assertFailsWith { - stripe.confirmPaymentIntentSuspend(mock()) + stripe.confirmPaymentIntent(mock()) } } @@ -538,7 +546,6 @@ internal class StripeKotlinTest { } private companion object { - const val TEST_PUBLISHABLE_KEY = "test_publishable_key" const val TEST_IDEMPOTENCY_KEY = "test_idempotenc_key" const val TEST_STRIPE_ACCOUNT_ID = "test_account_id" } From ed3e7721cf162045ea40cf82521a0898b4797607 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Mon, 5 Apr 2021 14:10:13 -0700 Subject: [PATCH 5/6] resolve comments --- .../main/java/com/stripe/android/StripeKtx.kt | 7 +- .../java/com/stripe/android/StripeKtxTest.kt | 113 ++++++++++-------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/stripe/src/main/java/com/stripe/android/StripeKtx.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt index 3a1528413c3..03f47bf5aa3 100644 --- a/stripe/src/main/java/com/stripe/android/StripeKtx.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -23,6 +23,7 @@ import com.stripe.android.model.Source import com.stripe.android.model.SourceParams import com.stripe.android.model.StripeFile import com.stripe.android.model.StripeFileParams +import com.stripe.android.model.StripeModel import com.stripe.android.model.Token import com.stripe.android.networking.ApiRequest @@ -566,9 +567,11 @@ suspend fun Stripe.confirmPaymentIntent( /** * Consume the empty result from Stripe's internal Json Parser, throw [InvalidRequestException] for public API. + * + * @return the result if the API result and JSON parsing are successful; otherwise, throw an exception. */ -private suspend inline fun runApiRequest( - crossinline block: suspend () -> T? +private inline fun runApiRequest( + block: () -> T? ): T = runCatching { requireNotNull(block()) { diff --git a/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt b/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt index 846b734a963..76cfa0ce096 100644 --- a/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt +++ b/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt @@ -8,14 +8,16 @@ import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.model.Source +import com.stripe.android.model.StripeModel import com.stripe.android.networking.ApiRequest import com.stripe.android.networking.StripeApiRepository import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import kotlin.test.AfterTest import kotlin.test.assertFailsWith import kotlin.test.assertSame @@ -28,6 +30,8 @@ internal class StripeKtxTest { private val mockApiRepository: StripeApiRepository = mock() private val mockPaymentController: PaymentController = mock() + private val testDispatcher = TestCoroutineDispatcher() + private val stripe: Stripe = Stripe( mockApiRepository, @@ -37,6 +41,11 @@ internal class StripeKtxTest { TestCoroutineDispatcher() ) + @AfterTest + fun cleanup() { + testDispatcher.cleanupTestCoroutines() + } + @Test fun `When repository returns correct value then createPaymentMethod should Succeed`(): Unit = `Given repository returns non-empty value when calling createAPI then returns correct result`( @@ -262,14 +271,14 @@ internal class StripeKtxTest { ) @Test - fun `When repository returns correct value then retrieveSource should Succeed`() = runBlocking { + fun `When repository returns correct value then retrieveSource should Succeed`() = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( mockApiRepository.retrieveSource(any(), any(), any()) ).thenReturn(expectedApiObj) val actualObj = stripe.retrieveSource( - "Dummy String param1", - "Dummy String param2", + "param11", + "param12", TEST_STRIPE_ACCOUNT_ID ) @@ -278,15 +287,15 @@ internal class StripeKtxTest { @Test fun `When repository throws exception then retrieveSource should throw same exception`(): Unit = - runBlocking { + testDispatcher.runBlockingTest { whenever( mockApiRepository.retrieveSource(any(), any(), any()) ).thenThrow(mock()) assertFailsWith { stripe.retrieveSource( - "Dummy String param1", - "Dummy String param2", + "param11", + "param12", TEST_STRIPE_ACCOUNT_ID ) } @@ -294,15 +303,15 @@ internal class StripeKtxTest { @Test fun `When repository returns null then retrieveSource should throw APIException`(): Unit = - runBlocking { + testDispatcher.runBlockingTest { whenever( mockApiRepository.retrieveSource(any(), any(), any()) ).thenReturn(null) assertFailsWith { stripe.retrieveSource( - "Dummy String param1", - "Dummy String param2", + "param11", + "param12", TEST_STRIPE_ACCOUNT_ID ) } @@ -310,7 +319,7 @@ internal class StripeKtxTest { @Test fun `When repository returns correct value then confirmSetupIntentSuspend should Succeed`() = - runBlocking { + testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) @@ -322,7 +331,7 @@ internal class StripeKtxTest { @Test fun `When repository throws exception then confirmSetupIntentSuspend should throw same exception`(): Unit = - runBlocking { + testDispatcher.runBlockingTest { whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenThrow(mock()) @@ -334,7 +343,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then confirmSetupIntentSuspend should throw APIException`(): Unit = - runBlocking { + testDispatcher.runBlockingTest { whenever( mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenReturn(null) @@ -346,7 +355,7 @@ internal class StripeKtxTest { @Test fun `When repository returns correct value then confirmPaymentIntentSuspend should Succeed`() = - runBlocking { + testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) @@ -358,7 +367,7 @@ internal class StripeKtxTest { @Test fun `When repository throws exception then confirmPaymentIntentSuspend should throw same exception`(): Unit = - runBlocking { + testDispatcher.runBlockingTest { whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenThrow(mock()) @@ -370,7 +379,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then confirmPaymentIntentSuspend should throw APIException`(): Unit = - runBlocking { + testDispatcher.runBlockingTest { whenever( mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenReturn(null) @@ -380,11 +389,11 @@ internal class StripeKtxTest { } } - private inline fun + private inline fun `Given repository returns non-empty value when calling createAPI then returns correct result`( crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject - ) = runBlocking { + ) = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( @@ -400,11 +409,11 @@ internal class StripeKtxTest { assertSame(expectedApiObj, actualObj) } - private inline fun + private inline fun `Given repository throws exception when calling createAPI then throws same exception`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, - crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject - ): Unit = runBlocking { + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, + crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> StripeModel + ): Unit = testDispatcher.runBlockingTest { whenever( repositoryInvocationBlock(any(), any()) ).thenThrow(mock()) @@ -418,11 +427,11 @@ internal class StripeKtxTest { } } - private inline fun + private inline fun `Given repository returns empty value when calling createAPI then thorws APIException`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, - crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject - ): Unit = runBlocking { + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, + crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> StripeModel + ): Unit = testDispatcher.runBlockingTest { whenever( repositoryInvocationBlock(any(), any()) ).thenReturn(null) @@ -436,11 +445,11 @@ internal class StripeKtxTest { } } - private inline fun + private inline fun `Given repository returns non-empty value when calling createAPI with String param then returns correct result`( crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject - ) = runBlocking { + ) = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( @@ -448,7 +457,7 @@ internal class StripeKtxTest { ).thenReturn(expectedApiObj) val actualObj = createAPIInvocationBlock( - "Dummy String param", + "param1", TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID ) @@ -456,47 +465,47 @@ internal class StripeKtxTest { assertSame(expectedApiObj, actualObj) } - private inline fun + private inline fun `Given repository throws exception when calling createAPI with String param then throws same exception`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, - crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject - ): Unit = runBlocking { + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, + crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> StripeModel + ): Unit = testDispatcher.runBlockingTest { whenever( repositoryInvocationBlock(any(), any()) ).thenThrow(mock()) assertFailsWith { createAPIInvocationBlock( - "Dummy String param", + "param1", TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID ) } } - private inline fun + private inline fun `Given repository returns empty value when calling createAPI with String param then throws APIException`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, - crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject - ): Unit = runBlocking { + crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, + crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> StripeModel + ): Unit = testDispatcher.runBlockingTest { whenever( repositoryInvocationBlock(any(), any()) ).thenReturn(null) assertFailsWith { createAPIInvocationBlock( - "Dummy String param", + "param1", TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID ) } } - private inline fun + private inline fun `Given repository returns non-empty value when calling retrieveAPI with String param then returns correct result`( crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject - ) = runBlocking { + ): Unit = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( @@ -504,42 +513,42 @@ internal class StripeKtxTest { ).thenReturn(expectedApiObj) val actualObj = retrieveAPIInvocationBlock( - "Dummy String param", + "param1", TEST_STRIPE_ACCOUNT_ID ) assertSame(expectedApiObj, actualObj) } - private inline fun + private fun `Given repository throws exception when calling retrieveAPI with String param then throws same exception`( - crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, - crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject - ): Unit = runBlocking { + repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> StripeModel?, + retrieveAPIInvocationBlock: suspend (String, String?) -> StripeModel + ): Unit = testDispatcher.runBlockingTest { whenever( repositoryInvocationBlock(any(), any(), any()) ).thenThrow(mock()) assertFailsWith { retrieveAPIInvocationBlock( - "Dummy String param", + "param1", TEST_STRIPE_ACCOUNT_ID ) } } - private inline fun + private fun `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( - crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, - crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject - ): Unit = runBlocking { + repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> StripeModel?, + retrieveAPIInvocationBlock: suspend (String, String?) -> StripeModel + ): Unit = testDispatcher.runBlockingTest { whenever( repositoryInvocationBlock(any(), any(), any()) ).thenReturn(null) assertFailsWith { retrieveAPIInvocationBlock( - "Dummy String param", + "param1", TEST_STRIPE_ACCOUNT_ID ) } From c86f0a06bb7e0f3e4828b38275073e4694bdf585 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Mon, 5 Apr 2021 15:50:24 -0700 Subject: [PATCH 6/6] resolve comments --- .../main/java/com/stripe/android/StripeKtx.kt | 8 +- .../java/com/stripe/android/StripeKtxTest.kt | 173 +++++++++++------- 2 files changed, 106 insertions(+), 75 deletions(-) diff --git a/stripe/src/main/java/com/stripe/android/StripeKtx.kt b/stripe/src/main/java/com/stripe/android/StripeKtx.kt index 03f47bf5aa3..adc48f34f71 100644 --- a/stripe/src/main/java/com/stripe/android/StripeKtx.kt +++ b/stripe/src/main/java/com/stripe/android/StripeKtx.kt @@ -570,11 +570,11 @@ suspend fun Stripe.confirmPaymentIntent( * * @return the result if the API result and JSON parsing are successful; otherwise, throw an exception. */ -private inline fun runApiRequest( - block: () -> T? -): T = +private inline fun runApiRequest( + block: () -> APIObject? +): APIObject = runCatching { requireNotNull(block()) { - "Failed to parse ${T::class.java.simpleName}." + "Failed to parse ${APIObject::class.java.simpleName}." } }.getOrElse { throw StripeException.create(it) } diff --git a/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt b/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt index 76cfa0ce096..9c85c98a0af 100644 --- a/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt +++ b/stripe/src/test/java/com/stripe/android/StripeKtxTest.kt @@ -1,5 +1,6 @@ package com.stripe.android +import com.google.common.truth.Truth.assertThat import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever @@ -8,7 +9,9 @@ import com.stripe.android.exception.InvalidRequestException import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.model.Source +import com.stripe.android.model.StripeFile import com.stripe.android.model.StripeModel +import com.stripe.android.model.StripeParamsModel import com.stripe.android.networking.ApiRequest import com.stripe.android.networking.StripeApiRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -38,7 +41,7 @@ internal class StripeKtxTest { mockPaymentController, ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, TEST_STRIPE_ACCOUNT_ID, - TestCoroutineDispatcher() + testDispatcher ) @AfterTest @@ -62,7 +65,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createPaymentMethod should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( + `Given repository returns null when calling createAPI then throws APIException`( mockApiRepository::createPaymentMethod, stripe::createPaymentMethod ) @@ -83,7 +86,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createSource should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( + `Given repository returns null when calling createAPI then throws APIException`( mockApiRepository::createSource, stripe::createSource ) @@ -104,7 +107,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createAccountToken should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( + `Given repository returns null when calling createAPI then throws APIException`( mockApiRepository::createToken, stripe::createAccountToken ) @@ -125,7 +128,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createBankAccountToken should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( + `Given repository returns null when calling createAPI then throws APIException`( mockApiRepository::createToken, stripe::createBankAccountToken ) @@ -146,7 +149,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createPiiToken should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI with String param then throws APIException`( + `Given repository returns null when calling createAPI with String param then throws APIException`( mockApiRepository::createToken, stripe::createPiiToken ) @@ -167,7 +170,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createCardToken should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( + `Given repository returns null when calling createAPI then throws APIException`( mockApiRepository::createToken, stripe::createCardToken ) @@ -188,7 +191,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createCvcUpdateToken should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI with String param then throws APIException`( + `Given repository returns null when calling createAPI with String param then throws APIException`( mockApiRepository::createToken, stripe::createCvcUpdateToken ) @@ -209,24 +212,44 @@ internal class StripeKtxTest { @Test fun `When repository returns null then createPersonToken should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( + `Given repository returns null when calling createAPI then throws APIException`( mockApiRepository::createToken, stripe::createPersonToken ) @Test fun `When repository returns correct value then createFile should Succeed`(): Unit = - `Given repository returns non-empty value when calling createAPI then returns correct result`( - mockApiRepository::createFile, - stripe::createFile - ) + testDispatcher.runBlockingTest { + val expectedFile = mock() + + whenever( + mockApiRepository.createFile(any(), any()) + ).thenReturn(expectedFile) + + val actualFile = stripe.createFile( + mock(), + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + + assertSame(expectedFile, actualFile) + } @Test fun `When repository returns null then createFile should throw APIException`(): Unit = - `Given repository returns empty value when calling createAPI then thorws APIException`( - mockApiRepository::createFile, - stripe::createFile - ) + testDispatcher.runBlockingTest { + whenever( + mockApiRepository.createFile(any(), any()) + ).thenReturn(null) + + assertFailsWithMessage("Failed to parse StripeFile.") { + stripe.createFile( + mock(), + TEST_IDEMPOTENCY_KEY, + TEST_STRIPE_ACCOUNT_ID + ) + } + } @Test fun `When repository returns correct value then retrievePaymentIntent should Succeed`(): Unit = @@ -244,7 +267,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then retrievePaymentIntent should throw APIException`(): Unit = - `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( + `Given repository returns null when calling retrieveAPI with String param then throws APIException`( mockApiRepository::retrievePaymentIntent, stripe::retrievePaymentIntent ) @@ -265,7 +288,7 @@ internal class StripeKtxTest { @Test fun `When repository returns null then retrieveSetupIntent should throw APIException`(): Unit = - `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( + `Given repository returns null when calling retrieveAPI with String param then throws APIException`( mockApiRepository::retrieveSetupIntent, stripe::retrieveSetupIntent ) @@ -308,7 +331,7 @@ internal class StripeKtxTest { mockApiRepository.retrieveSource(any(), any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWithMessage("Failed to parse Source.") { stripe.retrieveSource( "param11", "param12", @@ -348,7 +371,7 @@ internal class StripeKtxTest { mockApiRepository.confirmSetupIntent(any(), any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWithMessage("Failed to parse SetupIntent.") { stripe.confirmSetupIntent(mock()) } } @@ -384,23 +407,23 @@ internal class StripeKtxTest { mockApiRepository.confirmPaymentIntent(any(), any(), any()) ).thenReturn(null) - assertFailsWith { + assertFailsWithMessage("Failed to parse PaymentIntent.") { stripe.confirmPaymentIntent(mock()) } } - private inline fun + private inline fun `Given repository returns non-empty value when calling createAPI then returns correct result`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, - crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject + crossinline repositoryBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createApiInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject ) = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( - repositoryInvocationBlock(any(), any()) + repositoryBlock(any(), any()) ).thenReturn(expectedApiObj) - val actualObj = createAPIInvocationBlock( + val actualObj = createApiInvocationBlock( mock(), TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID @@ -409,17 +432,17 @@ internal class StripeKtxTest { assertSame(expectedApiObj, actualObj) } - private inline fun + private inline fun `Given repository throws exception when calling createAPI then throws same exception`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, - crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> StripeModel + crossinline repositoryBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, + crossinline createApiInvocationBlock: suspend (CreateAPIParam, String?, String?) -> StripeModel ): Unit = testDispatcher.runBlockingTest { whenever( - repositoryInvocationBlock(any(), any()) + repositoryBlock(any(), any()) ).thenThrow(mock()) assertFailsWith { - createAPIInvocationBlock( + createApiInvocationBlock( mock(), TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID @@ -427,17 +450,17 @@ internal class StripeKtxTest { } } - private inline fun - `Given repository returns empty value when calling createAPI then thorws APIException`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, - crossinline createAPIInvocationBlock: suspend (CreateAPIParam, String?, String?) -> StripeModel + private inline fun + `Given repository returns null when calling createAPI then throws APIException`( + crossinline repositoryBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createApiInvocationBlock: suspend (CreateAPIParam, String?, String?) -> APIObject ): Unit = testDispatcher.runBlockingTest { whenever( - repositoryInvocationBlock(any(), any()) + repositoryBlock(any(), any()) ).thenReturn(null) - assertFailsWith { - createAPIInvocationBlock( + assertFailsWithMessage("Failed to parse ${APIObject::class.java.simpleName}.") { + createApiInvocationBlock( mock(), TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID @@ -445,18 +468,18 @@ internal class StripeKtxTest { } } - private inline fun + private inline fun `Given repository returns non-empty value when calling createAPI with String param then returns correct result`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, - crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> APIObject + crossinline repositoryBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createApiInvocationBlock: suspend (String, String?, String?) -> APIObject ) = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( - repositoryInvocationBlock(any(), any()) + repositoryBlock(any(), any()) ).thenReturn(expectedApiObj) - val actualObj = createAPIInvocationBlock( + val actualObj = createApiInvocationBlock( "param1", TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID @@ -465,17 +488,17 @@ internal class StripeKtxTest { assertSame(expectedApiObj, actualObj) } - private inline fun + private inline fun `Given repository throws exception when calling createAPI with String param then throws same exception`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, - crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> StripeModel + crossinline repositoryBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, + crossinline createApiInvocationBlock: suspend (String, String?, String?) -> StripeModel ): Unit = testDispatcher.runBlockingTest { whenever( - repositoryInvocationBlock(any(), any()) + repositoryBlock(any(), any()) ).thenThrow(mock()) assertFailsWith { - createAPIInvocationBlock( + createApiInvocationBlock( "param1", TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID @@ -483,17 +506,17 @@ internal class StripeKtxTest { } } - private inline fun - `Given repository returns empty value when calling createAPI with String param then throws APIException`( - crossinline repositoryInvocationBlock: suspend (RepositoryParam, ApiRequest.Options) -> StripeModel?, - crossinline createAPIInvocationBlock: suspend (String, String?, String?) -> StripeModel + private inline fun + `Given repository returns null when calling createAPI with String param then throws APIException`( + crossinline repositoryBlock: suspend (RepositoryParam, ApiRequest.Options) -> APIObject?, + crossinline createApiInvocationBlock: suspend (String, String?, String?) -> APIObject ): Unit = testDispatcher.runBlockingTest { whenever( - repositoryInvocationBlock(any(), any()) + repositoryBlock(any(), any()) ).thenReturn(null) - assertFailsWith { - createAPIInvocationBlock( + assertFailsWithMessage("Failed to parse ${APIObject::class.java.simpleName}.") { + createApiInvocationBlock( "param1", TEST_IDEMPOTENCY_KEY, TEST_STRIPE_ACCOUNT_ID @@ -503,16 +526,16 @@ internal class StripeKtxTest { private inline fun `Given repository returns non-empty value when calling retrieveAPI with String param then returns correct result`( - crossinline repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, - crossinline retrieveAPIInvocationBlock: suspend (String, String?) -> APIObject + crossinline repositoryBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, + crossinline retrieveApiInvocationBlock: suspend (String, String?) -> APIObject ): Unit = testDispatcher.runBlockingTest { val expectedApiObj = mock() whenever( - repositoryInvocationBlock(any(), any(), any()) + repositoryBlock(any(), any(), any()) ).thenReturn(expectedApiObj) - val actualObj = retrieveAPIInvocationBlock( + val actualObj = retrieveApiInvocationBlock( "param1", TEST_STRIPE_ACCOUNT_ID ) @@ -522,38 +545,46 @@ internal class StripeKtxTest { private fun `Given repository throws exception when calling retrieveAPI with String param then throws same exception`( - repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> StripeModel?, - retrieveAPIInvocationBlock: suspend (String, String?) -> StripeModel + repositoryBlock: suspend (String, ApiRequest.Options, List) -> StripeModel?, + retrieveApiInvocationBlock: suspend (String, String?) -> StripeModel ): Unit = testDispatcher.runBlockingTest { whenever( - repositoryInvocationBlock(any(), any(), any()) + repositoryBlock(any(), any(), any()) ).thenThrow(mock()) assertFailsWith { - retrieveAPIInvocationBlock( + retrieveApiInvocationBlock( "param1", TEST_STRIPE_ACCOUNT_ID ) } } - private fun - `Given repository returns empty value when calling retrieveAPI with String param then throws APIException`( - repositoryInvocationBlock: suspend (String, ApiRequest.Options, List) -> StripeModel?, - retrieveAPIInvocationBlock: suspend (String, String?) -> StripeModel + private inline fun + `Given repository returns null when calling retrieveAPI with String param then throws APIException`( + crossinline repositoryBlock: suspend (String, ApiRequest.Options, List) -> APIObject?, + crossinline retrieveApiInvocationBlock: suspend (String, String?) -> APIObject ): Unit = testDispatcher.runBlockingTest { whenever( - repositoryInvocationBlock(any(), any(), any()) + repositoryBlock(any(), any(), any()) ).thenReturn(null) - assertFailsWith { - retrieveAPIInvocationBlock( + assertFailsWith("Failed to parse ${APIObject::class.java.simpleName}.") { + retrieveApiInvocationBlock( "param1", TEST_STRIPE_ACCOUNT_ID ) } } + private inline fun assertFailsWithMessage( + throwableMsg: String, + block: () -> Unit + ) { + val throwable = assertFailsWith { block() } + assertThat(throwable.message).isEqualTo(throwableMsg) + } + private companion object { const val TEST_IDEMPOTENCY_KEY = "test_idempotenc_key" const val TEST_STRIPE_ACCOUNT_ID = "test_account_id"