diff --git a/stripe/src/main/java/com/stripe/android/IssuingCardPinService.kt b/stripe/src/main/java/com/stripe/android/IssuingCardPinService.kt index feef8ec29f5..723a4b66446 100644 --- a/stripe/src/main/java/com/stripe/android/IssuingCardPinService.kt +++ b/stripe/src/main/java/com/stripe/android/IssuingCardPinService.kt @@ -8,7 +8,11 @@ import com.stripe.android.Stripe.Companion.appInfo import com.stripe.android.exception.InvalidRequestException import com.stripe.android.networking.StripeApiRepository import com.stripe.android.networking.StripeRepository -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext /** * Methods for retrieval / update of a Stripe Issuing card @@ -16,7 +20,8 @@ import kotlinx.coroutines.runBlocking class IssuingCardPinService @VisibleForTesting internal constructor( keyProvider: EphemeralKeyProvider, private val stripeRepository: StripeRepository, - private val operationIdFactory: OperationIdFactory = StripeOperationIdFactory() + private val operationIdFactory: OperationIdFactory = StripeOperationIdFactory(), + private val workContext: CoroutineContext = Dispatchers.IO ) { private val retrievalListeners = mutableMapOf() private val updateListeners = mutableMapOf() @@ -133,8 +138,8 @@ class IssuingCardPinService @VisibleForTesting internal constructor( operation: EphemeralOperation.Issuing.RetrievePin, listener: IssuingCardPinRetrievalListener ) { - runCatching { - runBlocking { + CoroutineScope(workContext).launch { + runCatching { requireNotNull( stripeRepository.retrieveIssuingCardPin( operation.cardId, @@ -145,19 +150,23 @@ class IssuingCardPinService @VisibleForTesting internal constructor( ) { "Could not retrieve issuing card PIN." } - } - }.fold( - onSuccess = listener::onIssuingCardPinRetrieved, - onFailure = { - onRetrievePinError(it, listener) - } - ) + }.fold( + onSuccess = { pin -> + withContext(Dispatchers.Main) { + listener.onIssuingCardPinRetrieved(pin) + } + }, + onFailure = { + onRetrievePinError(it, listener) + } + ) + } } - private fun onRetrievePinError( + private suspend fun onRetrievePinError( throwable: Throwable, listener: IssuingCardPinRetrievalListener - ) { + ) = withContext(Dispatchers.Main) { when (throwable) { is InvalidRequestException -> { when (throwable.stripeError?.code) { @@ -213,8 +222,8 @@ class IssuingCardPinService @VisibleForTesting internal constructor( operation: EphemeralOperation.Issuing.UpdatePin, listener: IssuingCardPinUpdateListener ) { - runCatching { - runBlocking { + CoroutineScope(workContext).launch { + runCatching { stripeRepository.updateIssuingCardPin( operation.cardId, operation.newPin, @@ -222,18 +231,23 @@ class IssuingCardPinService @VisibleForTesting internal constructor( operation.userOneTimeCode, ephemeralKey.secret ) - } - }.fold( - onSuccess = { - listener.onIssuingCardPinUpdated() - }, - onFailure = { - onUpdatePinError(it, listener) - } - ) + }.fold( + onSuccess = { + withContext(Dispatchers.Main) { + listener.onIssuingCardPinUpdated() + } + }, + onFailure = { + onUpdatePinError(it, listener) + } + ) + } } - private fun onUpdatePinError(throwable: Throwable, listener: IssuingCardPinUpdateListener) { + private suspend fun onUpdatePinError( + throwable: Throwable, + listener: IssuingCardPinUpdateListener + ) = withContext(Dispatchers.Main) { when (throwable) { is InvalidRequestException -> { when (throwable.stripeError?.code) { diff --git a/stripe/src/test/java/com/stripe/android/IssuingCardPinServiceTest.kt b/stripe/src/test/java/com/stripe/android/IssuingCardPinServiceTest.kt index d3c694a3f22..8748fe6cf4d 100644 --- a/stripe/src/test/java/com/stripe/android/IssuingCardPinServiceTest.kt +++ b/stripe/src/test/java/com/stripe/android/IssuingCardPinServiceTest.kt @@ -1,68 +1,58 @@ package com.stripe.android -import androidx.test.core.app.ApplicationProvider -import com.nhaarman.mockitokotlin2.argThat +import com.google.common.truth.Truth.assertThat import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import com.stripe.android.networking.ApiRequest -import com.stripe.android.networking.ApiRequestExecutor -import com.stripe.android.networking.ApiRequestMatcher -import com.stripe.android.networking.StripeApiRepository -import com.stripe.android.networking.StripeRequest -import com.stripe.android.networking.StripeResponse +import com.stripe.android.exception.InvalidRequestException +import com.stripe.android.networking.AbsFakeStripeRepository import com.stripe.android.testharness.TestEphemeralKeyProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.json.JSONObject import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test /** * Test class for [IssuingCardPinService]. */ +@ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class IssuingCardPinServiceTest { - private val stripeApiRequestExecutor: ApiRequestExecutor = mock() private val retrievalListener: IssuingCardPinService.IssuingCardPinRetrievalListener = mock() private val updateListener: IssuingCardPinService.IssuingCardPinUpdateListener = mock() - private val stripeRepository = StripeApiRepository( - ApplicationProvider.getApplicationContext(), - ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, - stripeApiRequestExecutor = stripeApiRequestExecutor, - analyticsRequestExecutor = {} - ) + private val testDispatcher = TestCoroutineDispatcher() + + private val stripeRepository = FakeStripeRepository() private val service = IssuingCardPinService( TestEphemeralKeyProvider().also { it.setNextRawEphemeralKey(EPHEMERAL_KEY.toString()) }, stripeRepository, - OperationIdFactory.get() + OperationIdFactory.get(), + testDispatcher ) - @Test - fun testRetrieval() { - val response = StripeResponse( - 200, - """ - { - "card": "ic_abcdef", - "pin": "1234" - } - """.trimIndent() - ) + @BeforeTest + fun setup() { + Dispatchers.setMain(testDispatcher) + } - whenever( - stripeApiRequestExecutor.execute( - argThat( - ApiRequestMatcher( - StripeRequest.Method.GET, - "https://api.stripe.com/v1/issuing/cards/ic_abcdef/pin?verification%5Bone_time_code%5D=123-456&verification%5Bid%5D=iv_abcd", - ApiRequest.Options("ek_test_123") - ) - ) - ) - ).thenReturn(response) + @AfterTest + fun cleanup() { + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } + + @Test + fun `retrievePin() should call onIssuingCardPinRetrieved() on listener when successful`() { + stripeRepository.retrievedPin = { PIN } service.retrievePin( "ic_abcdef", @@ -72,36 +62,14 @@ class IssuingCardPinServiceTest { ) verify(retrievalListener) - .onIssuingCardPinRetrieved("1234") + .onIssuingCardPinRetrieved(PIN) } @Test - fun testUpdate() { - val response = StripeResponse( - 200, - """ - { - "card": "ic_abcdef", - "pin": "" - } - """.trimIndent() - ) - - whenever( - stripeApiRequestExecutor.execute( - argThat( - ApiRequestMatcher( - StripeRequest.Method.POST, - "https://api.stripe.com/v1/issuing/cards/ic_abcdef/pin", - ApiRequest.Options("ek_test_123") - ) - ) - ) - ).thenReturn(response) - + fun `updatePin() should call onIssuingCardPinUpdated() on listener when successful`() { service.updatePin( "ic_abcdef", - "1234", + "5678", "iv_abcd", "123-456", updateListener @@ -109,34 +77,22 @@ class IssuingCardPinServiceTest { verify(updateListener) .onIssuingCardPinUpdated() + + assertThat(stripeRepository.updatePinCalls) + .isEqualTo(1) } @Test - fun testRetrievalFailsWithReason() { - val response = StripeResponse( - 400, - """ - { - "error": { - "code": "incorrect_code", - "message": "Verification failed", - "type": "invalid_request_error" - } - } - """.trimIndent() - ) - - whenever( - stripeApiRequestExecutor.execute( - argThat( - ApiRequestMatcher( - StripeRequest.Method.GET, - "https://api.stripe.com/v1/issuing/cards/ic_abcdef/pin?verification%5Bone_time_code%5D=123-456&verification%5Bid%5D=iv_abcd", - ApiRequest.Options("ek_test_123") - ) + fun `retrievePin() should call onError() on listener when there is an error`() { + stripeRepository.retrievedPin = { + throw InvalidRequestException( + stripeError = StripeError( + code = "incorrect_code", + message = "Verification failed", + type = "invalid_request_error" ) ) - ).thenReturn(response) + } service.retrievePin( "ic_abcdef", @@ -152,7 +108,31 @@ class IssuingCardPinServiceTest { ) } + private class FakeStripeRepository : AbsFakeStripeRepository() { + var retrievedPin: () -> String? = { null } + var updatePinCalls = 0 + + override suspend fun retrieveIssuingCardPin( + cardId: String, + verificationId: String, + userOneTimeCode: String, + ephemeralKeySecret: String + ): String? = retrievedPin() + + override suspend fun updateIssuingCardPin( + cardId: String, + newPin: String, + verificationId: String, + userOneTimeCode: String, + ephemeralKeySecret: String + ) { + updatePinCalls++ + } + } + private companion object { + private const val PIN = "1234" + private val EPHEMERAL_KEY = JSONObject( """ {