From 42993379a779b70c38fa58ff80bb6ea9c6d6739a Mon Sep 17 00:00:00 2001 From: Danilo Joksimovic Date: Mon, 17 Jun 2024 13:58:07 -0400 Subject: [PATCH] Fix tests from merging against Devin's changes --- .../services/vault/AbstractVaultSubmitter.kt | 2 +- .../vault/forage/RosettaPinSubmitter.kt | 25 ++-- .../ecom/ui/vault/forage/RosettaPinElement.kt | 30 +++-- .../android/ui/RosettaPinElementTests.kt | 7 +- .../vault/AbstractVaultSubmitterTest.kt | 110 +++++++++++------- .../android/vault/RosettaPinSubmitterTest.kt | 25 ++-- 6 files changed, 117 insertions(+), 82 deletions(-) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt index 731cca1e..e8516e74 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/AbstractVaultSubmitter.kt @@ -154,7 +154,7 @@ internal abstract class AbstractVaultSubmitter( } val successfulResponse = parser.successfulResponse - if (successfulResponse != null) { + if (successfulResponse != null) { logger.i("[$vaultType] Received successful response from $vaultType") return successfulResponse } diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaPinSubmitter.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaPinSubmitter.kt index 2c6c8798..76d2345a 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaPinSubmitter.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaPinSubmitter.kt @@ -1,7 +1,7 @@ package com.joinforage.forage.android.ecom.services.vault.forage -import android.content.Context import android.widget.EditText +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.addPathSegmentsSafe @@ -14,10 +14,9 @@ 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.SecurePinCollector 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 import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -27,13 +26,13 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject internal class RosettaPinSubmitter( - context: Context, - foragePinEditText: ForagePinElement, + private val editText: EditText, + collector: SecurePinCollector, + private val envConfig: EnvConfig, logger: Log, - private val vaultUrlBuilder: ((String) -> HttpUrl) = { path -> buildVaultUrl(path) } + private val vaultUrlBuilder: ((String) -> HttpUrl) = { path -> buildVaultUrl(envConfig, path) } ) : AbstractVaultSubmitter( - context = context, - foragePinEditText = foragePinEditText, + collector = collector, logger = logger ) { override val vaultType: VaultType = VaultType.FORAGE_VAULT_TYPE @@ -47,7 +46,7 @@ internal class RosettaPinSubmitter( return try { val apiUrl = vaultUrlBuilder(vaultProxyRequest.path) val baseRequestBody = buildBaseRequestBody(vaultProxyRequest) - val requestBody = buildForageVaultRequestBody(foragePinEditText, baseRequestBody) + val requestBody = buildForageVaultRequestBody(editText, baseRequestBody) val request = Request.Builder() .url(apiUrl) @@ -91,17 +90,17 @@ internal class RosettaPinSubmitter( companion object { // this code assumes that .setForageConfig() has been called // on a Forage***EditText before VAULT_BASE_URL gets referenced - private fun buildVaultUrl(path: String): HttpUrl = - StopgapGlobalState.envConfig.vaultBaseUrl.toHttpUrlOrNull()!! + private fun buildVaultUrl(envConfig: EnvConfig, path: String): HttpUrl = + envConfig.vaultBaseUrl.toHttpUrlOrNull()!! .newBuilder() .addPathSegment("proxy") .addPathSegmentsSafe(path) .addTrailingSlash() .build() - private fun buildForageVaultRequestBody(foragePinEditText: ForagePinElement, baseRequestBody: Map): RequestBody { + private fun buildForageVaultRequestBody(editText: EditText, baseRequestBody: Map): RequestBody { val jsonBody = JSONObject(baseRequestBody) - jsonBody.put("pin", (foragePinEditText.getTextElement() as EditText).text) + jsonBody.put("pin", editText.text) val mediaType = "application/json".toMediaTypeOrNull() return jsonBody.toString().toRequestBody(mediaType) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/forage/RosettaPinElement.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/forage/RosettaPinElement.kt index ab9d82ed..90c98de2 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/forage/RosettaPinElement.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/forage/RosettaPinElement.kt @@ -16,12 +16,12 @@ import android.widget.EditText import android.widget.LinearLayout import androidx.core.content.getSystemService import com.joinforage.forage.android.R +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.VaultType 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.SecurePinCollector import com.joinforage.forage.android.core.ui.VaultWrapper -import com.joinforage.forage.android.core.ui.element.ForagePinElement -import com.joinforage.forage.android.core.ui.element.state.PinElementStateManager import com.joinforage.forage.android.core.ui.getBoxCornerRadiusBottomEnd import com.joinforage.forage.android.core.ui.getBoxCornerRadiusBottomStart import com.joinforage.forage.android.core.ui.getBoxCornerRadiusTopEnd @@ -37,8 +37,6 @@ internal class RosettaPinElement @JvmOverloads constructor( override val vaultType: VaultType = VaultType.FORAGE_VAULT_TYPE private val _editText: EditText - override val manager: PinElementStateManager = PinElementStateManager.forEmptyInput() - init { context.obtainStyledAttributes(attrs, R.styleable.ForagePINEditText, defStyleAttr, 0).apply { try { @@ -51,11 +49,17 @@ internal class RosettaPinElement @JvmOverloads constructor( } override fun getVaultSubmitter( - foragePinElement: ForagePinElement, + envConfig: EnvConfig, logger: Log ): AbstractVaultSubmitter = RosettaPinSubmitter( - foragePinElement.context, - foragePinElement, + editText = _editText, + object : SecurePinCollector { + override fun clearText() { + this@RosettaPinElement.clearText() + } + override fun isComplete(): Boolean = inputState.isComplete + }, + envConfig, logger ) @@ -194,14 +198,22 @@ internal class RosettaPinElement @JvmOverloads constructor( private fun registerFocusChangeListener() { _editText.setOnFocusChangeListener { _, hasFocus -> - manager.changeFocus(hasFocus) + focusState = focusState.changeFocus(hasFocus) + focusState.fireEvent( + onFocusEventListener = onFocusEventListener, + onBlurEventListener = onBlurEventListener + ) } } private fun registerTextWatcher() { val pinTextWatcher = PinTextWatcher(_editText) pinTextWatcher.onInputChangeEvent { isComplete, isEmpty -> - manager.handleChangeEvent(isComplete, isEmpty) + inputState = inputState.handleChangeEvent( + isComplete = isComplete, + isEmpty = isEmpty + ) + onChangeEventListener?.invoke(pinEditTextState) } _editText.addTextChangedListener(pinTextWatcher) } diff --git a/forage-android/src/test/java/com/joinforage/forage/android/ui/RosettaPinElementTests.kt b/forage-android/src/test/java/com/joinforage/forage/android/ui/RosettaPinElementTests.kt index 0d2d3f3f..9c9e7767 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/ui/RosettaPinElementTests.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/ui/RosettaPinElementTests.kt @@ -91,10 +91,13 @@ class RosettaPinElementTests { val focusChangeListener = editText.onFocusChangeListener focusChangeListener.onFocusChange(editText, true) - assertTrue(rosettaPinElement.manager.isFocused) + + assertTrue(rosettaPinElement.pinEditTextState.isFocused) + assertFalse(rosettaPinElement.pinEditTextState.isBlurred) focusChangeListener.onFocusChange(editText, false) - assertFalse(rosettaPinElement.manager.isFocused) + assertFalse(rosettaPinElement.pinEditTextState.isFocused) + assertTrue(rosettaPinElement.pinEditTextState.isBlurred) } /** diff --git a/forage-android/src/test/java/com/joinforage/forage/android/vault/AbstractVaultSubmitterTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/vault/AbstractVaultSubmitterTest.kt index 091aaa3f..5d514021 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/vault/AbstractVaultSubmitterTest.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/vault/AbstractVaultSubmitterTest.kt @@ -1,6 +1,5 @@ package com.joinforage.forage.android.vault -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.forageapi.network.ForageApiResponse @@ -9,11 +8,9 @@ import com.joinforage.forage.android.core.services.forageapi.paymentmethod.Payme import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.telemetry.UserAction import com.joinforage.forage.android.core.services.vault.AbstractVaultSubmitter +import com.joinforage.forage.android.core.services.vault.SecurePinCollector 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.state.INITIAL_PIN_ELEMENT_STATE -import com.joinforage.forage.android.core.ui.element.state.pin.PinEditTextState -import com.joinforage.forage.android.ecom.ui.element.ForagePINEditText import com.joinforage.forage.android.mock.MockLogger import com.joinforage.forage.android.mock.MockServiceFactory import kotlinx.coroutines.test.runTest @@ -23,16 +20,14 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` class AbstractVaultSubmitterTest : MockServerSuite() { private lateinit var mockLogger: MockLogger - private lateinit var mockForagePinEditText: ForagePINEditText - private lateinit var mockContext: Context private lateinit var abstractVaultSubmitter: AbstractVaultSubmitter + private val mockCollector = object : SecurePinCollector { + override fun clearText() {} + override fun isComplete(): Boolean = true + } companion object { private val mockEncryptionKeys = EncryptionKeys("vgs-alias", "bt-alias") @@ -53,25 +48,24 @@ class AbstractVaultSubmitterTest : MockServerSuite() { super.setup() mockLogger = MockLogger() - mockForagePinEditText = mock(ForagePINEditText::class.java) - mockContext = mock(Context::class.java) - - val state = PinEditTextState.forEmptyInput(FocusState) - state.isComplete = true - `when`(mockForagePinEditText.getElementState()).thenReturn(state) - abstractVaultSubmitter = ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) } @Test fun `submit with invalid PIN returns IncompletePinError`() = runTest { - val state = INITIAL_PIN_ELEMENT_STATE.copy(isComplete = false) + val incompleteCollector = object : SecurePinCollector { + override fun clearText() {} + override fun isComplete(): Boolean = false + } + + val abstractVaultSubmitter = ConcreteVaultSubmitter( + collector = incompleteCollector, + logger = mockLogger + ) - `when`(mockForagePinEditText.getElementState()).thenReturn(state) val response = abstractVaultSubmitter.submit(mockVaultParams) val forageError = (response as ForageApiResponse.Failure).errors.first() @@ -82,8 +76,7 @@ class AbstractVaultSubmitterTest : MockServerSuite() { @Test fun `submit with successful vault proxy response returns Success`() = runTest { val concreteSubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { override suspend fun submitProxyRequest( @@ -101,8 +94,7 @@ class AbstractVaultSubmitterTest : MockServerSuite() { @Test fun `submit with failed proxy response returns Failure`() = runTest { val concreteVaultSubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { override suspend fun submitProxyRequest( @@ -120,8 +112,7 @@ class AbstractVaultSubmitterTest : MockServerSuite() { @Test fun `submit with missing vault token returns UnknownErrorApiResponse`() = runTest { val concreteSubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { // Mock missing token @@ -139,17 +130,57 @@ class AbstractVaultSubmitterTest : MockServerSuite() { } @Test - fun `calls clearText after submitting`() = runTest { - abstractVaultSubmitter.submit(mockVaultParams) + fun `calls clearText after submitting on success`() = runTest { + var numTimesClearTextCalled = 0 + val clearTextSpyCollector = object : SecurePinCollector { + override fun clearText() { + numTimesClearTextCalled += 1 + } + override fun isComplete(): Boolean = true + } - verify(mockForagePinEditText, times(1)).clearText() + val successVaultSubmitter = object : ConcreteVaultSubmitter( + collector = clearTextSpyCollector, + logger = mockLogger + ) { + override suspend fun submitProxyRequest( + vaultProxyRequest: VaultProxyRequest + ): ForageApiResponse { + return ForageApiResponse.Success("success") + } + } + successVaultSubmitter.submit(mockVaultParams) + assertEquals(1, numTimesClearTextCalled) + } + + @Test + fun `calls clearText after submitting on failure`() = runTest { + var numTimesClearTextCalled = 0 + val clearTextSpyCollector = object : SecurePinCollector { + override fun clearText() { + numTimesClearTextCalled += 1 + } + override fun isComplete(): Boolean = true + } + + val failedVaultSubmitter = object : ConcreteVaultSubmitter( + collector = clearTextSpyCollector, + logger = mockLogger + ) { + override suspend fun submitProxyRequest( + vaultProxyRequest: VaultProxyRequest + ): ForageApiResponse { + return UnknownErrorApiResponse + } + } + failedVaultSubmitter.submit(mockVaultParams) + assertEquals(1, numTimesClearTextCalled) } @Test fun `grabs the correct vault token`() = runTest { val basisTheorySubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { override fun getVaultToken(paymentMethod: PaymentMethod): String? { @@ -158,8 +189,7 @@ class AbstractVaultSubmitterTest : MockServerSuite() { } val vgsSubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { override fun getVaultToken(paymentMethod: PaymentMethod): String? { @@ -177,8 +207,7 @@ class AbstractVaultSubmitterTest : MockServerSuite() { @Test fun `success metrics event is reported`() = runTest { val concreteVaultSubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { override suspend fun submitProxyRequest( @@ -206,8 +235,7 @@ class AbstractVaultSubmitterTest : MockServerSuite() { @Test fun `failure metrics event is reported`() = runTest { val concreteVaultSubmitter = object : ConcreteVaultSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + collector = mockCollector, logger = mockLogger ) { override suspend fun submitProxyRequest( @@ -247,12 +275,10 @@ class AbstractVaultSubmitterTest : MockServerSuite() { } internal open class ConcreteVaultSubmitter( - context: Context, - foragePinEditText: ForagePINEditText, + collector: SecurePinCollector, logger: Log ) : AbstractVaultSubmitter( - context = context, - foragePinEditText = foragePinEditText, + collector = collector, logger = logger ) { override val vaultType: VaultType = VaultType.VGS_VAULT_TYPE diff --git a/forage-android/src/test/java/com/joinforage/forage/android/vault/RosettaPinSubmitterTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/vault/RosettaPinSubmitterTest.kt index 740746f9..c361b857 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/vault/RosettaPinSubmitterTest.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/vault/RosettaPinSubmitterTest.kt @@ -1,8 +1,8 @@ package com.joinforage.forage.android.vault -import android.content.Context import android.text.Editable import android.widget.EditText +import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.ForageConstants import com.joinforage.forage.android.core.services.addPathSegmentsSafe import com.joinforage.forage.android.core.services.addTrailingSlash @@ -10,12 +10,10 @@ import com.joinforage.forage.android.core.services.forageapi.network.ForageApiRe import com.joinforage.forage.android.core.services.forageapi.paymentmethod.EbtCard import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod import com.joinforage.forage.android.core.services.telemetry.UserAction -import com.joinforage.forage.android.core.services.vault.StopgapGlobalState +import com.joinforage.forage.android.core.services.vault.SecurePinCollector 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.ForageConfig import com.joinforage.forage.android.ecom.services.vault.forage.RosettaPinSubmitter -import com.joinforage.forage.android.ecom.ui.ForagePINEditText import com.joinforage.forage.android.fixtures.givenRosettaPaymentCaptureRequest import com.joinforage.forage.android.fixtures.returnsMalformedError import com.joinforage.forage.android.fixtures.returnsPayment @@ -38,12 +36,11 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.json.JSONObject import org.junit.Before import org.junit.Test -import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.Mockito.`when` class RosettaPinSubmitterTest() : MockServerSuite() { private lateinit var mockLogger: MockLogger - private lateinit var mockForagePinEditText: ForagePINEditText private lateinit var submitter: RosettaPinSubmitter @Before @@ -54,20 +51,20 @@ class RosettaPinSubmitterTest() : MockServerSuite() { // Use Mockito judiciously (mainly for mocking views)! // Opt for dependency injection and inheritance over Mockito, when possible - mockForagePinEditText = Mockito.mock(ForagePINEditText::class.java) - val mockContext = Mockito.mock(Context::class.java) // Mock the PIN value! - val mockEditText = Mockito.mock(EditText::class.java) - `when`(mockForagePinEditText.getTextElement()).thenReturn(mockEditText) - val mockEditable = Mockito.mock(Editable::class.java) + val mockEditText = mock(EditText::class.java) + val mockEditable = mock(Editable::class.java) `when`(mockEditable.toString()).thenReturn("1234") `when`(mockEditText.text).thenReturn(mockEditable) + val mockCollector = mock(SecurePinCollector::class.java) + submitter = RosettaPinSubmitter( - context = mockContext, - foragePinEditText = mockForagePinEditText, + editText = mockEditText, + collector = mockCollector, logger = mockLogger, + envConfig = EnvConfig.Local, // Ensure we don't make any LIVE requests!!! // Emulates the real vaultUrlBuilder, but using the empty test URL vaultUrlBuilder = { path -> @@ -78,8 +75,6 @@ class RosettaPinSubmitterTest() : MockServerSuite() { .build() } ) - - StopgapGlobalState.forageConfig = ForageConfig(mockData.merchantId, mockData.sessionToken) } @Test