Skip to content

Commit

Permalink
Invoke issuing API requests on background thread
Browse files Browse the repository at this point in the history
Previously requests were being fired on the main thread
(`runBlocking`). Move requests to a background thread and dispatch
results on main thread.

Fixes #3499
  • Loading branch information
mshafrir-stripe committed Mar 23, 2021
1 parent d640ee1 commit 5706d5d
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 108 deletions.
67 changes: 41 additions & 26 deletions stripe/src/main/java/com/stripe/android/IssuingCardPinService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ 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
*/
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
) {
private val retrievalListeners = mutableMapOf<String, IssuingCardPinRetrievalListener>()
private val updateListeners = mutableMapOf<String, IssuingCardPinUpdateListener>()
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -213,27 +222,32 @@ class IssuingCardPinService @VisibleForTesting internal constructor(
operation: EphemeralOperation.Issuing.UpdatePin,
listener: IssuingCardPinUpdateListener
) {
runCatching {
runBlocking {
CoroutineScope(workContext).launch {
runCatching {
stripeRepository.updateIssuingCardPin(
operation.cardId,
operation.newPin,
operation.verificationId,
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) {
Expand Down Expand Up @@ -345,7 +359,8 @@ class IssuingCardPinService @VisibleForTesting internal constructor(
return IssuingCardPinService(
keyProvider,
StripeApiRepository(context, publishableKey, appInfo),
StripeOperationIdFactory()
StripeOperationIdFactory(),
Dispatchers.IO
)
}
}
Expand Down
142 changes: 60 additions & 82 deletions stripe/src/test/java/com/stripe/android/IssuingCardPinServiceTest.kt
Original file line number Diff line number Diff line change
@@ -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
)

@BeforeTest
fun setup() {
Dispatchers.setMain(testDispatcher)
}

@AfterTest
fun cleanup() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}

@Test
fun testRetrieval() {
val response = StripeResponse(
200,
"""
{
"card": "ic_abcdef",
"pin": "1234"
}
""".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")
)
)
)
).thenReturn(response)
stripeRepository.retrievedPin = { "1234" }

service.retrievePin(
"ic_abcdef",
Expand All @@ -77,28 +67,6 @@ class IssuingCardPinServiceTest {

@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)

service.updatePin(
"ic_abcdef",
"1234",
Expand All @@ -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")
)
stripeRepository.retrievedPin = {
throw InvalidRequestException(
stripeError = StripeError(
code = "incorrect_code",
message = "Verification failed",
type = "invalid_request_error"
)
)
).thenReturn(response)
}

service.retrievePin(
"ic_abcdef",
Expand All @@ -152,6 +108,28 @@ 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 val EPHEMERAL_KEY = JSONObject(
"""
Expand Down

0 comments on commit 5706d5d

Please sign in to comment.