From 89099c9df7ea91caadab0c94bbfbdc341e162689 Mon Sep 17 00:00:00 2001 From: Samer Alabi Date: Fri, 4 Oct 2024 14:01:33 -0400 Subject: [PATCH] Catch all errors coming from `CustomerAdapter` --- .../data/CustomerAdapterDataSource.kt | 58 +++++-- .../customersheet/FakeCustomerAdapter.kt | 10 +- .../data/CustomerAdapterDataSourceTest.kt | 154 +++++++++++++++++- 3 files changed, 203 insertions(+), 19 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerAdapterDataSource.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerAdapterDataSource.kt index 6071b2959fb..b634ee94733 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerAdapterDataSource.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerAdapterDataSource.kt @@ -17,6 +17,7 @@ import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.model.SavedSelection import com.stripe.android.paymentsheet.repositories.ElementsSessionRepository import kotlinx.coroutines.async +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.CoroutineContext @@ -66,37 +67,51 @@ internal class CustomerAdapterDataSource @Inject constructor( }.toCustomerSheetDataResult() } - override suspend fun retrievePaymentMethods(): CustomerSheetDataResult> { - return customerAdapter.retrievePaymentMethods().toCustomerSheetDataResult() + override suspend fun retrievePaymentMethods() = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.retrievePaymentMethods() + }.toCustomerSheetDataResult() } override suspend fun updatePaymentMethod( paymentMethodId: String, params: PaymentMethodUpdateParams, - ): CustomerSheetDataResult { - return customerAdapter.updatePaymentMethod(paymentMethodId, params).toCustomerSheetDataResult() + ) = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.updatePaymentMethod(paymentMethodId, params) + }.toCustomerSheetDataResult() } - override suspend fun attachPaymentMethod(paymentMethodId: String): CustomerSheetDataResult { - return customerAdapter.attachPaymentMethod(paymentMethodId).toCustomerSheetDataResult() + override suspend fun attachPaymentMethod(paymentMethodId: String) = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.attachPaymentMethod(paymentMethodId) + }.toCustomerSheetDataResult() } - override suspend fun detachPaymentMethod(paymentMethodId: String): CustomerSheetDataResult { - return customerAdapter.detachPaymentMethod(paymentMethodId).toCustomerSheetDataResult() + override suspend fun detachPaymentMethod(paymentMethodId: String) = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.detachPaymentMethod(paymentMethodId) + }.toCustomerSheetDataResult() } - override suspend fun retrieveSavedSelection(): CustomerSheetDataResult { - return customerAdapter.retrieveSelectedPaymentOption().map { result -> - result?.toSavedSelection() + override suspend fun retrieveSavedSelection() = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.retrieveSelectedPaymentOption().map { result -> + result?.toSavedSelection() + } }.toCustomerSheetDataResult() } - override suspend fun setSavedSelection(selection: SavedSelection?): CustomerSheetDataResult { - return customerAdapter.setSelectedPaymentOption(selection?.toPaymentOption()).toCustomerSheetDataResult() + override suspend fun setSavedSelection(selection: SavedSelection?) = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.setSelectedPaymentOption(selection?.toPaymentOption()) + }.toCustomerSheetDataResult() } - override suspend fun retrieveSetupIntentClientSecret(): CustomerSheetDataResult { - return customerAdapter.setupIntentClientSecretForCustomerAttach().toCustomerSheetDataResult() + override suspend fun retrieveSetupIntentClientSecret() = withContext(workContext) { + runCatchingAdapterTask { + customerAdapter.setupIntentClientSecretForCustomerAttach() + }.toCustomerSheetDataResult() } private suspend fun fetchElementsSession(): Result { @@ -149,4 +164,17 @@ internal class CustomerAdapterDataSource @Inject constructor( listOf("card") } } + + private suspend fun runCatchingAdapterTask( + task: suspend () -> CustomerAdapter.Result + ): CustomerAdapter.Result { + return runCatching { + task() + }.fold( + onSuccess = { it }, + onFailure = { + CustomerAdapter.Result.failure(cause = it, displayMessage = null) + } + ) + } } diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/FakeCustomerAdapter.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/FakeCustomerAdapter.kt index ecab9e77da0..2502caaf27e 100644 --- a/paymentsheet/src/test/java/com/stripe/android/customersheet/FakeCustomerAdapter.kt +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/FakeCustomerAdapter.kt @@ -12,6 +12,12 @@ internal class FakeCustomerAdapter( CustomerAdapter.Result.success(null), private val paymentMethods: CustomerAdapter.Result> = CustomerAdapter.Result.success(listOf(CARD_PAYMENT_METHOD)), + private val onPaymentMethods: (() -> CustomerAdapter.Result>)? = { + paymentMethods + }, + private val onGetPaymentOption: (() -> CustomerAdapter.Result)? = { + selectedPaymentOption + }, private val onSetSelectedPaymentOption: ((paymentOption: CustomerAdapter.PaymentOption?) -> CustomerAdapter.Result)? = null, private val onAttachPaymentMethod: ((paymentMethodId: String) -> CustomerAdapter.Result)? = null, @@ -22,7 +28,7 @@ internal class FakeCustomerAdapter( ) : CustomerAdapter { override suspend fun retrievePaymentMethods(): CustomerAdapter.Result> { - return paymentMethods + return onPaymentMethods?.invoke() ?: paymentMethods } override suspend fun attachPaymentMethod(paymentMethodId: String): CustomerAdapter.Result { @@ -57,7 +63,7 @@ internal class FakeCustomerAdapter( } override suspend fun retrieveSelectedPaymentOption(): CustomerAdapter.Result { - return selectedPaymentOption + return onGetPaymentOption?.invoke() ?: selectedPaymentOption } override suspend fun setupIntentClientSecretForCustomerAttach(): CustomerAdapter.Result { diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerAdapterDataSourceTest.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerAdapterDataSourceTest.kt index c3d71b7c3b7..db0cd24f747 100644 --- a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerAdapterDataSourceTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerAdapterDataSourceTest.kt @@ -63,6 +63,27 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on retrieve payment methods, should catch and fail if an exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to get payment methods!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onPaymentMethods = { + throw exception + } + ) + ) + + val result = dataSource.retrievePaymentMethods() + + assertThat(result).isInstanceOf>>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on retrieve payment option, should complete successfully from adapter`() = runTest { val paymentOptionId = "pm_1" @@ -105,6 +126,27 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on retrieve payment option, should catch and fail if an exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to retrieve saved selection!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onGetPaymentOption = { + throw exception + } + ) + ) + + val result = dataSource.retrieveSavedSelection() + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on set saved selection, should complete successfully from adapter`() = runTest { val dataSource = createCustomerAdapterDataSource( @@ -117,7 +159,7 @@ class CustomerAdapterDataSourceTest { val result = dataSource.setSavedSelection(SavedSelection.GooglePay) - assertThat(result).isInstanceOf>() + assertThat(result).isInstanceOf>() } @Test @@ -135,7 +177,7 @@ class CustomerAdapterDataSourceTest { val result = dataSource.setSavedSelection(SavedSelection.GooglePay) - assertThat(result).isInstanceOf>() + assertThat(result).isInstanceOf>() val failedResult = result.asFailure() @@ -144,6 +186,27 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on set saved selection, should catch and fail if an exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to set selection!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onSetSelectedPaymentOption = { + throw exception + } + ) + ) + + val result = dataSource.setSavedSelection(SavedSelection.GooglePay) + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on attach payment method, should complete successfully from adapter`() = runTest { val paymentMethod = PaymentMethodFactory.card(id = "pm_1") @@ -188,6 +251,27 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on attach payment method, should catch and fail if an exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to attach!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onAttachPaymentMethod = { + throw exception + } + ) + ) + + val result = dataSource.attachPaymentMethod(paymentMethodId = "pm_1") + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on detach payment method, should complete successfully from adapter`() = runTest { val paymentMethod = PaymentMethodFactory.card(id = "pm_1") @@ -232,6 +316,27 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on detach payment method, should catch and fail if an exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to detach!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onDetachPaymentMethod = { + throw exception + } + ) + ) + + val result = dataSource.detachPaymentMethod(paymentMethodId = "pm_1") + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on update payment method, should complete successfully from adapter`() = runTest { val paymentMethod = PaymentMethodFactory.card(id = "pm_1") @@ -282,6 +387,30 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on update payment method, should catch and fail if an exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to update!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onUpdatePaymentMethod = { _, _ -> + throw exception + } + ) + ) + + val result = dataSource.updatePaymentMethod( + paymentMethodId = "pm_1", + params = PaymentMethodUpdateParams.createCard(expiryYear = 2028, expiryMonth = 7), + ) + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on can create setup intents, should return true from adapter`() = runTest { val dataSource = createCustomerAdapterDataSource( @@ -347,6 +476,27 @@ class CustomerAdapterDataSourceTest { assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!") } + @Test + fun `on fetch setup intent client secret, should catch & fail if exception is thrown from adapter`() = runTest { + val exception = IllegalStateException("Failed to update!") + + val dataSource = createCustomerAdapterDataSource( + adapter = FakeCustomerAdapter( + onSetupIntentClientSecretForCustomerAttach = { + throw exception + } + ) + ) + + val result = dataSource.retrieveSetupIntentClientSecret() + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + @Test fun `on load customer sheet session, should load properly & report success events`() = runTest { val intent = SetupIntentFactory.create()