From 8b4f9245132dcabe8c6cd486f77499ff84aea33c Mon Sep 17 00:00:00 2001 From: Devin Morgan Date: Mon, 20 May 2024 08:59:54 -0400 Subject: [PATCH] Formally delete StopgapGlobalState This commit finally passes down the relevant EnvConfig info to the PinSubmitters allowing us to delete the global state we had been using before and eliminating the chance for bugs, which we've experienced in the past Signed-off-by: Devin Morgan --- .../core/services/vault/StopgapGlobalState.kt | 10 - .../core/ui/element/AbstractForageElement.kt | 8 +- .../vault/bt/BasisTheoryPinSubmitter.kt | 23 +- .../services/vault/vgs/VgsPinSubmitter.kt | 25 +-- .../ecom/ui/vault/bt/BTVaultWrapper.kt | 1 + .../ecom/ui/vault/vgs/VGSVaultWrapper.kt | 1 + .../vault/BasisTheoryPinSubmitterTest.kt | 201 ------------------ 7 files changed, 17 insertions(+), 252 deletions(-) delete mode 100644 forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt delete mode 100644 forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt deleted file mode 100644 index b9fe7a24b..000000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/StopgapGlobalState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joinforage.forage.android.core.services.vault - -import com.joinforage.forage.android.core.services.EnvConfig -import com.joinforage.forage.android.core.ui.element.ForageConfig - -internal object StopgapGlobalState { - var forageConfig: ForageConfig? = null - val envConfig: EnvConfig - get() = EnvConfig.fromForageConfig(forageConfig) -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt index 018305341..7ea1a562d 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/ui/element/AbstractForageElement.kt @@ -3,7 +3,7 @@ package com.joinforage.forage.android.core.ui.element import android.content.Context import android.util.AttributeSet import android.widget.LinearLayout -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.ui.element.state.ElementState /** @@ -34,11 +34,6 @@ abstract class AbstractForageElement( // update the forage config this._forageConfig = forageConfig - // TODO: 9/20/23: This is a temporary workaround and is - // not meant to stick around. See this doc for more details - // https://www.notion.so/joinforage/226d8ee6f8294d2694b1bb451791960b - StopgapGlobalState.forageConfig = forageConfig - // there are a number of side effect operations that we // need to run as soon as a ForageElement has access to // ForageConfig data. However, we don't want to run these @@ -60,4 +55,5 @@ abstract class AbstractForageElement( internal fun getForageConfig(): ForageConfig? { return _forageConfig } + internal fun getEnvConfig(): EnvConfig = EnvConfig.fromForageConfig(_forageConfig) } diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt index d78a05ab8..20164b329 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt @@ -3,6 +3,7 @@ package com.joinforage.forage.android.ecom.services.vault.bt import com.basistheory.android.service.BasisTheoryElements import com.basistheory.android.service.ProxyRequest import com.basistheory.android.view.TextElement +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.ForageConstants import com.joinforage.forage.android.core.services.VaultType import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys @@ -10,7 +11,6 @@ import com.joinforage.forage.android.core.services.forageapi.network.ForageApiRe import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState import com.joinforage.forage.android.core.services.vault.VaultProxyRequest import com.joinforage.forage.android.core.services.vault.VaultSubmitterParams import com.joinforage.forage.android.core.ui.element.ForagePinElement @@ -19,8 +19,8 @@ internal typealias BasisTheoryResponse = Result internal class BasisTheoryPinSubmitter( foragePinEditText: ForagePinElement, + private val envConfig: EnvConfig, logger: Log, - private val buildVaultProvider: () -> BasisTheoryElements = { buildBt() } ) : AbstractVaultSubmitter( foragePinEditText = foragePinEditText, logger = logger @@ -40,11 +40,13 @@ internal class BasisTheoryPinSubmitter( encryptionKey = encryptionKey, vaultToken = vaultToken ) - .setHeader(ForageConstants.Headers.BT_PROXY_KEY, PROXY_ID) + .setHeader(ForageConstants.Headers.BT_PROXY_KEY, envConfig.btProxyID) .setHeader(ForageConstants.Headers.CONTENT_TYPE, "application/json") override suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse { - val bt = buildVaultProvider() + val bt = BasisTheoryElements.builder() + .apiKey(envConfig.btAPIKey) + .build() val proxyRequest: ProxyRequest = ProxyRequest().apply { headers = vaultProxyRequest.headers @@ -64,17 +66,4 @@ internal class BasisTheoryPinSubmitter( override fun getVaultToken(paymentMethod: PaymentMethod): String? = pickVaultTokenByIndex(paymentMethod, 1) - companion object { - // this code assumes that .setForageConfig() has been called - // on a Forage***EditText before PROXY_ID or API_KEY get - // referenced - private val PROXY_ID = StopgapGlobalState.envConfig.btProxyID - private val API_KEY = StopgapGlobalState.envConfig.btAPIKey - - private fun buildBt(): BasisTheoryElements { - return BasisTheoryElements.builder() - .apiKey(API_KEY) - .build() - } - } } diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt index f345c9793..e94df5d24 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt @@ -1,8 +1,8 @@ package com.joinforage.forage.android.ecom.services.vault.vgs -import android.content.Context import com.joinforage.forage.android.core.services.VaultType import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.forageapi.network.ForageApiError import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.network.ForageError @@ -10,7 +10,6 @@ import com.joinforage.forage.android.core.services.forageapi.network.UnknownErro import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState import com.joinforage.forage.android.core.services.vault.VaultProxyRequest import com.joinforage.forage.android.core.ui.element.ForagePinElement import com.verygoodsecurity.vgscollect.VGSCollectLogger @@ -25,8 +24,8 @@ import kotlin.coroutines.suspendCoroutine internal class VgsPinSubmitter( foragePinEditText: ForagePinElement, + private val envConfig: EnvConfig, logger: Log, - private val buildVaultProvider: (context: Context) -> VGSCollect = { buildVGSCollect(foragePinEditText.context) } ) : AbstractVaultSubmitter( foragePinEditText = foragePinEditText, logger = logger @@ -35,7 +34,11 @@ internal class VgsPinSubmitter( override suspend fun submitProxyRequest( vaultProxyRequest: VaultProxyRequest ): ForageApiResponse = suspendCoroutine { continuation -> - val vgsCollect = buildVaultProvider(foragePinEditText.context) + VGSCollectLogger.isEnabled = false + val vgsCollect = VGSCollect + .Builder(foragePinEditText.context, envConfig.vgsVaultId) + .setEnvironment(envConfig.vgsVaultType) + .create() vgsCollect.bindView(foragePinEditText.getTextElement() as VGSEditText) vgsCollect.addOnResponseListeners(object : VgsCollectResponseListener { @@ -66,20 +69,6 @@ internal class VgsPinSubmitter( override fun getVaultToken(paymentMethod: PaymentMethod): String? = pickVaultTokenByIndex(paymentMethod, 0) - companion object { - // this code assumes that .setForageConfig() has been called - // on a Forage***EditText before PROXY_ID or API_KEY get - // referenced - private val VAULT_ID = StopgapGlobalState.envConfig.vgsVaultId - private val VGS_ENVIRONMENT = StopgapGlobalState.envConfig.vgsVaultType - - private fun buildVGSCollect(context: Context): VGSCollect { - VGSCollectLogger.isEnabled = false - return VGSCollect.Builder(context, VAULT_ID) - .setEnvironment(VGS_ENVIRONMENT) - .create() - } - } fun toVaultErrorOrNull(vaultResponse: VGSResponse?): ForageApiResponse.Failure? { if (vaultResponse == null) return null diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt index 2b5aef093..8b0b86796 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt @@ -116,6 +116,7 @@ internal class BTVaultWrapper @JvmOverloads constructor( logger: Log ): AbstractVaultSubmitter = BasisTheoryPinSubmitter( foragePinElement, + foragePinElement.getEnvConfig(), logger ) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt index f0d19da75..6d53555eb 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt @@ -136,6 +136,7 @@ internal class VGSVaultWrapper @JvmOverloads constructor( logger: Log ): AbstractVaultSubmitter = VgsPinSubmitter( foragePinElement, + foragePinElement.getEnvConfig(), logger ) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt deleted file mode 100644 index 67defec09..000000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.joinforage.forage.android.vault - -import android.content.Context -import com.basistheory.android.service.BasisTheoryElements -import com.basistheory.android.service.ProxyApi -import com.basistheory.android.service.ProxyRequest -import com.basistheory.android.view.TextElement -import com.joinforage.forage.android.core.services.ForageConstants -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState -import com.joinforage.forage.android.core.services.vault.VaultProxyRequest -import com.joinforage.forage.android.core.ui.element.ForageConfig -import com.joinforage.forage.android.ecom.services.vault.bt.BasisTheoryPinSubmitter -import com.joinforage.forage.android.ecom.services.vault.bt.BasisTheoryResponse -import com.joinforage.forage.android.ecom.services.vault.bt.ProxyRequestObject -import com.joinforage.forage.android.ecom.ui.ForagePINEditText -import com.joinforage.forage.android.mock.MockLogger -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import me.jorgecastillo.hiroaki.internal.MockServerSuite -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq - -class BasisTheoryPinSubmitterTest() : MockServerSuite() { - private lateinit var mockLogger: MockLogger - private lateinit var mockForagePinEditText: ForagePINEditText - private lateinit var submitter: BasisTheoryPinSubmitter - private lateinit var mockBasisTheory: BasisTheoryElements - private lateinit var mockBasisTheoryTextElement: TextElement - private lateinit var mockApiProxy: ProxyApi - - @Before - fun setUp() { - super.setup() - - mockLogger = MockLogger() - - // Use Mockito judiciously (mainly for mocking views)! - // Opt for dependency injection and inheritance over Mockito - mockForagePinEditText = mock(ForagePINEditText::class.java) - val mockContext = mock(Context::class.java) - mockBasisTheory = mock(BasisTheoryElements::class.java) - mockBasisTheoryTextElement = mock(TextElement::class.java) - `when`(mockForagePinEditText.getTextElement()).thenReturn(mockBasisTheoryTextElement) - - // ensure we don't make any live requests! - mockBasisTheoryResponse(Result.success("success")) - - submitter = BasisTheoryPinSubmitter( - foragePinEditText = mockForagePinEditText, - logger = mockLogger, - buildVaultProvider = { mockBasisTheory } - ) - } - - private fun mockBasisTheoryResponse(response: BasisTheoryResponse) { - mockApiProxy = mock(ProxyApi::class.java) - `when`(mockBasisTheory.proxy).thenReturn(mockApiProxy) - runBlocking { - if (response.isSuccess) { - `when`(mockApiProxy.post(anyOrNull(), anyOrNull())).thenReturn(response.getOrNull()!!) - } else { - `when`(mockApiProxy.post(anyOrNull(), anyOrNull())).thenThrow(response.exceptionOrNull()!!) - } - } - } - - @Test - fun `Basis Theory request receives expected params`() = runTest { - StopgapGlobalState.forageConfig = ForageConfig("1234567", "sandbox_abcdefg123") - - val vaultProxyRequest = VaultProxyRequest.emptyRequest() - .setHeader(ForageConstants.Headers.X_KEY, "12320ce0-1a3c-4c64-970c-51ed7db34548") - .setHeader(ForageConstants.Headers.MERCHANT_ACCOUNT, "1234567") - .setHeader(ForageConstants.Headers.IDEMPOTENCY_KEY, "abcdef123") - .setHeader(ForageConstants.Headers.TRACE_ID, "65639248-03f2-498d-8aa8-9ebd1c60ee65") - .setToken("45320ce0-1a3c-4c64-970c-51ed7db34548") - .setPath("/api/payment_methods/defghij123/balance/") - .setHeader(ForageConstants.Headers.BT_PROXY_KEY, StopgapGlobalState.envConfig.btProxyID) - .setHeader(ForageConstants.Headers.CONTENT_TYPE, "application/json") - - runBlocking { - submitter.submitProxyRequest(vaultProxyRequest) - } - - val captor = argumentCaptor() - verify(mockApiProxy).post(captor.capture(), eq(null)) - val capturedRequest = captor.firstValue - - assertEquals( - ProxyRequestObject( - pin = mockBasisTheoryTextElement, - card_number_token = "45320ce0-1a3c-4c64-970c-51ed7db34548" - ), - capturedRequest.body - ) - assertEquals( - hashMapOf( - "X-KEY" to "12320ce0-1a3c-4c64-970c-51ed7db34548", - "Merchant-Account" to "1234567", - "IDEMPOTENCY-KEY" to "abcdef123", - "x-datadog-trace-id" to "65639248-03f2-498d-8aa8-9ebd1c60ee65", - "BT-PROXY-KEY" to "R1CNiogSdhnHeNq6ZFWrG1", // sandbox value of btProxyID - "Content-Type" to "application/json" - ), - capturedRequest.headers - ) - assertEquals("/api/payment_methods/defghij123/balance/", capturedRequest.path) - } - - @Test - fun `submitProxyRequest with valid input should return success`() = runTest { - val responseStr = """{"content_id":"32489e7e-13d9-499c-b017-f68a0122da95","message_type":"0200","status":"sent_to_proxy","failed":false,"errors":[]}""" - mockBasisTheoryResponse(Result.success(responseStr)) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Success) - assertEquals("[basis_theory] Received successful response from basis_theory", mockLogger.infoLogs.last().getMessage()) - assertEquals(responseStr, (result as ForageApiResponse.Success).data) - } - - @Test - fun `Basis Theory returns a vault error`() = runTest { - val basisTheoryErrorMessage = """ - Message: - HTTP response code: 400 - HTTP response body: {"proxy_error":{"errors":{"error":["Basis Theory Validation Error"]},"title":"One or more validation errors occurred.","status":400,"detail":"Bad Request"}} - HTTP response headers: ... - """.trimIndent() - mockBasisTheoryResponse(Result.failure(RuntimeException(basisTheoryErrorMessage))) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Failure) - val firstError = (result as ForageApiResponse.Failure).errors[0] - assertEquals("[basis_theory] Received error from basis_theory: java.lang.RuntimeException: $basisTheoryErrorMessage", mockLogger.errorLogs.last().getMessage()) - assertEquals("Unknown Server Error", firstError.message) - assertEquals(500, firstError.httpStatusCode) - assertEquals("unknown_server_error", firstError.code) - } - - @Test - fun `Basis Theory returns a ForageError`() = runTest { - val responseStr = """ - Message: - HTTP response code: 400 - HTTP response body: {"path": "/api/payments/abcdefg123/collect_pin/","errors": [{"code": "too_many_requests","message": "Request was throttled, please try again later."}]} - HTTP response headers: {"some": "header"} - """.trimIndent() - - mockBasisTheoryResponse(Result.failure(RuntimeException(responseStr))) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Failure) - val firstError = (result as ForageApiResponse.Failure).errors[0] - assertEquals( - """ - [basis_theory] Received ForageError from basis_theory: Code: too_many_requests - Message: Request was throttled, please try again later. - Status Code: 400 - Error Details (below): - null - """.trimIndent(), - mockLogger.errorLogs.last().getMessage() - ) - assertEquals("Request was throttled, please try again later.", firstError.message) - assertEquals(400, firstError.httpStatusCode) - assertEquals("too_many_requests", firstError.code) - } - - @Test - fun `Basis Theory responds with a malformed error`() = runTest { - val responseStr = """ - Malformed error! - """.trimIndent() - - mockBasisTheoryResponse(Result.failure(RuntimeException(responseStr))) - - val result = submitter.submitProxyRequest(VaultProxyRequest.emptyRequest()) - - assertTrue(result is ForageApiResponse.Failure) - val firstError = (result as ForageApiResponse.Failure).errors[0] - assertEquals( - "[basis_theory] Received malformed response from basis_theory: Failure(java.lang.RuntimeException: Malformed error!)", - mockLogger.errorLogs.last().getMessage() - ) - assertEquals("Unknown Server Error", firstError.message) - assertEquals(500, firstError.httpStatusCode) - assertEquals("unknown_server_error", firstError.code) - } -}