diff --git a/README.md b/README.md index fe6a72aa..5fcc56e0 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,4 @@ The `/sample-app/` folder in this repository contains a very simple integration - Minimum API Level Android 5.0 (API level 21) - [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) v1.6.4 - 3rd party libraries: - - [VGS-Collect-Android](https://github.com/verygoodsecurity/vgs-collect-android) v1.7.3 - - [Basis-Theory-Android](https://github.com/Basis-Theory/basistheory-android) v2.5.0 - - [OkHttp](https://github.com/square/okhttp) v4.10.0 - - [Launch Darkly](https://github.com/launchdarkly/android-client-sdk) v4.2.1 + - [OkHttp](https://github.com/square/okhttp) v4.10.0 \ No newline at end of file diff --git a/forage-android/build.gradle b/forage-android/build.gradle index 0689ae78..5ffa2d68 100644 --- a/forage-android/build.gradle +++ b/forage-android/build.gradle @@ -116,17 +116,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' - implementation 'com.launchdarkly:launchdarkly-android-client-sdk:4.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - // VGS Collect SDK - api 'com.verygoodsecurity:vgscollect:1.7.3' - - // Basis Theory SDK - implementation ('com.github.basis-theory:basistheory-android:4.2.2') { - // Unable to build without excluding this dependency - // based on advice from this thread: https://github.com/gradle/gradle/issues/3065#issuecomment-341418873 - exclude group: 'javax.ws.rs' - } implementation "javax.ws.rs:javax.ws.rs-api:2.1@jar" testImplementation 'androidx.test:core-ktx:1.5.0' diff --git a/forage-android/consumer-rules.pro b/forage-android/consumer-rules.pro deleted file mode 100644 index 18441077..00000000 --- a/forage-android/consumer-rules.pro +++ /dev/null @@ -1,5 +0,0 @@ -# This ensures that the ProxyRequestObject property names (like card_number_token) are preserved -# and are not obfuscated when consumed by clients who use ProGuard. --keepclassmembers class com.joinforage.forage.android.ecom.services.vault.bt.ProxyRequestObject { - *; -} \ No newline at end of file diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/EnvConfig.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/EnvConfig.kt index ae7bc0ad..b4a38de2 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/EnvConfig.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/EnvConfig.kt @@ -13,13 +13,8 @@ internal enum class EnvOption(val value: String) { internal sealed class EnvConfig( val FLAVOR: EnvOption, - val btProxyID: String, - val btAPIKey: String, - val vgsVaultId: String, - val vgsVaultType: String, val apiBaseUrl: String, val vaultBaseUrl: String, - val ldMobileKey: String, val ddClientToken: String ) { // For the time being, I figure we can consume BuildConfig in exactly @@ -29,73 +24,43 @@ internal sealed class EnvConfig( object Local : EnvConfig( FLAVOR = EnvOption.LOCAL, - btProxyID = "N31FZgKpYZpo3oQ6XiM6M6", - btAPIKey = "key_AZfcBuKUsV38PEeYu6ZV8x", - vgsVaultId = "tntlqkidhc6", - vgsVaultType = "sandbox", apiBaseUrl = "http://10.0.2.2:8000/", vaultBaseUrl = "http://10.0.2.2:3999/", - ldMobileKey = "mob-03e025cb-5b4e-4d97-8685-39a22316d601", ddClientToken = "pubf13cedf24ba2ad50d4b9cb0b0100bd4a" ) object Dev : EnvConfig( FLAVOR = EnvOption.DEV, - btProxyID = "N31FZgKpYZpo3oQ6XiM6M6", - btAPIKey = "key_AZfcBuKUsV38PEeYu6ZV8x", - vgsVaultId = "tntlqkidhc6", - vgsVaultType = "sandbox", apiBaseUrl = "https://api.dev.joinforage.app/", vaultBaseUrl = "https://vault.dev.joinforage.app/", - ldMobileKey = "mob-03e025cb-5b4e-4d97-8685-39a22316d601", ddClientToken = "pubf13cedf24ba2ad50d4b9cb0b0100bd4a" ) object Staging : EnvConfig( FLAVOR = EnvOption.STAGING, - btProxyID = "ScWvAUkp53xz7muae7fW5p", - btAPIKey = "key_6B4cvpcDCEeNDYNow9zH7c", - vgsVaultId = "tnteykuh975", - vgsVaultType = "sandbox", apiBaseUrl = "https://api.staging.joinforage.app/", vaultBaseUrl = "https://vault.staging.joinforage.app/", - ldMobileKey = "mob-a9903698-759b-48e2-86e1-c551e2b69118", ddClientToken = "pubf13cedf24ba2ad50d4b9cb0b0100bd4a" ) object Sandbox : EnvConfig( FLAVOR = EnvOption.SANDBOX, - btProxyID = "R1CNiogSdhnHeNq6ZFWrG1", - btAPIKey = "key_DQ5NfUAgiqzwX1pxqcrSzK", - vgsVaultId = "tntagcot4b1", - vgsVaultType = "sandbox", apiBaseUrl = "https://api.sandbox.joinforage.app/", vaultBaseUrl = "https://vault.sandbox.joinforage.app/", - ldMobileKey = "mob-22024b85-05b7-4e24-b290-a071310dfc3d", ddClientToken = "pubf13cedf24ba2ad50d4b9cb0b0100bd4a" ) object Cert : EnvConfig( FLAVOR = EnvOption.CERT, - btProxyID = "AFSMtyyTGLKgmdWwrLCENX", - btAPIKey = "key_NdWtkKrZqztEfJRkZA8dmw", - vgsVaultId = "tntpnht7psv", - vgsVaultType = "sandbox", apiBaseUrl = "https://api.cert.joinforage.app/", vaultBaseUrl = "https://vault.cert.joinforage.app/", - ldMobileKey = "mob-d2261a08-784b-4300-a45f-ce0e46324d66", ddClientToken = "pubf13cedf24ba2ad50d4b9cb0b0100bd4a" ) object Prod : EnvConfig( FLAVOR = EnvOption.PROD, - btProxyID = "UxbU4Jn2RmvCovABjwCwsa", - btAPIKey = "key_BypNREttGMPbZ1muARDUf4", - vgsVaultId = "tntbcrncmgi", - vgsVaultType = "live", apiBaseUrl = "https://api.joinforage.app/", vaultBaseUrl = "https://vault.joinforage.app/", - ldMobileKey = "mob-5c3dfa7a-fa6d-4cdf-93e8-d28ef8080696", ddClientToken = "pubf13cedf24ba2ad50d4b9cb0b0100bd4a" ) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/Utils.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/Utils.kt index 786098a2..a66c8b60 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/Utils.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/Utils.kt @@ -2,18 +2,6 @@ package com.joinforage.forage.android.core.services import okhttp3.HttpUrl import org.json.JSONObject -import kotlin.random.Random - -/** - * We generate a random jitter amount to add to our retry delay when polling for the status of - * Payments and Payment Methods so that we can avoid a thundering herd scenario in which there are - * several requests retrying at the same exact time. - * - * Returns a random integer between -25 and 25 - */ -internal fun getJitterAmount(random: Random = Random.Default): Int { - return random.nextInt(-25, 26) -} internal fun HttpUrl.Builder.addTrailingSlash(): HttpUrl.Builder { return this.addPathSegment("") @@ -22,14 +10,12 @@ internal fun HttpUrl.Builder.addTrailingSlash(): HttpUrl.Builder { internal object ForageConstants { object Headers { - const val X_KEY = "X-KEY" const val MERCHANT_ACCOUNT = "Merchant-Account" const val IDEMPOTENCY_KEY = "IDEMPOTENCY-KEY" const val TRACE_ID = "x-datadog-trace-id" const val AUTHORIZATION = "Authorization" const val BEARER = "Bearer" const val API_VERSION = "API-VERSION" - const val BT_PROXY_KEY = "BT-PROXY-KEY" const val CONTENT_TYPE = "Content-Type" const val SESSION_TOKEN = "Session-Token" } @@ -46,23 +32,14 @@ internal object ForageConstants { } object PathSegment { - const val ISO_SERVER = "iso_server" - const val ENCRYPTION_ALIAS = "encryption_alias" const val API = "api" const val PAYMENT_METHODS = "payment_methods" - const val MESSAGE = "message" const val PAYMENTS = "payments" const val REFUNDS = "refunds" } - - object VGS { - const val PIN_FIELD_NAME = "pin" - } } internal enum class VaultType(val value: String) { - VGS_VAULT_TYPE("vgs"), - BT_VAULT_TYPE("basis_theory"), FORAGE_VAULT_TYPE("forage"); override fun toString(): String { diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/encryptkey/EncryptionKeyService.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/encryptkey/EncryptionKeyService.kt deleted file mode 100644 index 8f81827f..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/encryptkey/EncryptionKeyService.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.joinforage.forage.android.core.services.forageapi.encryptkey - -import com.joinforage.forage.android.core.services.ForageConstants -import com.joinforage.forage.android.core.services.addTrailingSlash -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.NetworkService -import com.joinforage.forage.android.core.services.telemetry.Log -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import java.io.IOException - -internal class EncryptionKeyService( - private val httpUrl: String, - okHttpClient: OkHttpClient, - private val logger: Log -) : NetworkService(okHttpClient, logger) { - suspend fun getEncryptionKey(): ForageApiResponse = try { - logger.i("[HTTP] GET request for Encryption Key") - getEncryptionToCoroutine() - } catch (ex: IOException) { - logger.e("[HTTP] Failed while trying to GET Encryption Key", ex) - ForageApiResponse.Failure( - 500, - "unknown_server_error", - ex.message.orEmpty() - ) - } - - private suspend fun getEncryptionToCoroutine(): ForageApiResponse { - val url = getEncryptionKeyUrl() - - val request: Request = Request.Builder() - .url(url) - .get() - .build() - - return convertCallbackToCoroutine(request) - } - - private fun getEncryptionKeyUrl(): HttpUrl = httpUrl.toHttpUrlOrNull()!! - .newBuilder() - .addPathSegment(ForageConstants.PathSegment.ISO_SERVER) - .addPathSegment(ForageConstants.PathSegment.ENCRYPTION_ALIAS) - .addTrailingSlash() - .build() -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/encryptkey/EncryptionKeys.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/encryptkey/EncryptionKeys.kt deleted file mode 100644 index 16c61e0e..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/encryptkey/EncryptionKeys.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.joinforage.forage.android.core.services.forageapi.encryptkey - -import org.json.JSONObject - -internal data class EncryptionKeys( - val vgsAlias: String, - val btAlias: String -) { - object ModelMapper { - fun from(string: String): EncryptionKeys { - val jsonObject = JSONObject(string) - - val vgsAlias = jsonObject.getString("alias") - val btAlias = jsonObject.getString("bt_alias") - - return EncryptionKeys( - vgsAlias = vgsAlias, - btAlias = btAlias - ) - } - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/network/error/payload/SingleErrorResponsePayload.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/network/error/payload/SingleErrorResponsePayload.kt index 7cf3e6a6..24eb727e 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/network/error/payload/SingleErrorResponsePayload.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/network/error/payload/SingleErrorResponsePayload.kt @@ -10,7 +10,6 @@ import org.json.JSONObject * { * "ref": "e1fff94f29", * "balance": null, - * "content_id": "c1898593-fa3d-4a1c-b16b-10ecc38b3619", * "error": { * "message": "Invalid card number - Re-enter Transaction", * "forage_code": "ebt_error_14", diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/paymentmethod/Balance.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/paymentmethod/Balance.kt index 813e6f69..10177934 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/paymentmethod/Balance.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/forageapi/paymentmethod/Balance.kt @@ -41,8 +41,7 @@ data class EbtBalance( * "snap": "1000.00", * "non_snap": "1000.00", * "updated": "2024-07-11T07:50:27.355331-07:00" - * }, - * "content_id": "13d711a6-75af-4564-935c-05854f829b5e" + * } * } */ internal fun fromVaultResponse(res: ForageApiResponse.Success): EbtBalance { diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/telemetry/Metrics.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/telemetry/Metrics.kt index fe2dee1d..d8879d28 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/telemetry/Metrics.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/telemetry/Metrics.kt @@ -55,7 +55,7 @@ internal enum class EventOutcome(val value: String) { internal enum class EventName(val value: String) { /* - VAULT_RESPONSE refers to a response from the VGS or BT submit actions. + VAULT_RESPONSE refers to a response from the Rosetta submit actions. */ VAULT_RESPONSE("vault_response"), @@ -63,8 +63,7 @@ internal enum class EventName(val value: String) { CUSTOMER_PERCEIVED_RESPONSE refers to the response from a balance or capture action. There are multiple chained requests that come from the client when executing a balance or capture action. Ex of a balance action: - [GET] EncryptionKey -> [GET] PaymentMethod -> [POST] to VGS/BT -> [GET] Poll for Response -> - [GET] PaymentMethod -> Return Balance + [GET] PaymentMethod -> [POST] to Rosetta -> Return Balance */ CUSTOMER_PERCEIVED_RESPONSE("customer_perceived_response"); @@ -120,17 +119,15 @@ internal abstract class ResponseMonitor(metricsLogger: Log? = Log.getInstance } /* - VaultProxyResponseMonitor is used to track the response time from the VGS and BT submit - functions. The timer begins when a balance or capture request is submitted to VGS/BT + VaultProxyResponseMonitor is used to track the response time from the Rosetta submit + function. The timer begins when a balance or capture request is submitted to Rosetta and ends when a response is received by the SDK. */ -internal class VaultProxyResponseMonitor(vault: VaultType, userAction: UserAction, metricsLogger: Log?) : ResponseMonitor(metricsLogger) { - private var vaultType: VaultType? = null +internal class VaultProxyResponseMonitor(userAction: UserAction, metricsLogger: Log?) : ResponseMonitor(metricsLogger) { private var userAction: UserAction? = null private var eventName: EventName = EventName.VAULT_RESPONSE init { - this.vaultType = vault this.userAction = userAction } @@ -150,7 +147,7 @@ internal class VaultProxyResponseMonitor(vault: VaultType, userAction: UserActio return } - val vaultType = vaultType + val vaultType = VaultType.FORAGE_VAULT_TYPE val userAction = userAction val forageErrorCodeOrNull = forageErrorCode ?: UnknownForageErrorCode.UNKNOWN @@ -178,17 +175,15 @@ internal class VaultProxyResponseMonitor(vault: VaultType, userAction: UserActio that come from the client when executing a balance or capture action. The timer begins when the first HTTP request is sent from the SDK and ends when the the SDK returns information back to the user. Ex of a balance action: - Timer Begins -> [GET] EncryptionKey -> [GET] PaymentMethod -> [POST] to VGS/BT -> + Timer Begins -> [GET] PaymentMethod -> [POST] to Rosetta -> [GET] Poll for Response -> [GET] PaymentMethod -> Timer Ends -> Return Balance */ -internal class CustomerPerceivedResponseMonitor(vault: VaultType, userAction: UserAction, metricsLogger: Log?) : ResponseMonitor(metricsLogger) { - private var vaultType: VaultType? = null +internal class CustomerPerceivedResponseMonitor(userAction: UserAction, metricsLogger: Log?) : ResponseMonitor(metricsLogger) { private var userAction: UserAction? = null private var eventOutcome: EventOutcome? = null private var eventName: EventName = EventName.CUSTOMER_PERCEIVED_RESPONSE init { - this.vaultType = vault this.userAction = userAction } @@ -232,7 +227,7 @@ internal class CustomerPerceivedResponseMonitor(vault: VaultType, userAction: Us return } - val vaultType = vaultType + val vaultType = VaultType.FORAGE_VAULT_TYPE val userAction = userAction val forageErrorCodeOrNull = forageErrorCode ?: UnknownForageErrorCode.UNKNOWN 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 574047a4..6251e8b9 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 @@ -2,7 +2,6 @@ package com.joinforage.forage.android.core.services.vault 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 import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.network.UnknownErrorApiResponse import com.joinforage.forage.android.core.services.forageapi.paymentmethod.EbtCard @@ -18,7 +17,6 @@ internal val IncompletePinError = ForageApiResponse.Failure( ) internal open class VaultSubmitterParams( - open val encryptionKeys: EncryptionKeys, open val idempotencyKey: String, open val merchantId: String, open val path: String, @@ -56,7 +54,6 @@ internal abstract class AbstractVaultSubmitter( } val vaultToken = getVaultToken(params.paymentMethod) - val encryptionKey = parseEncryptionKey(params.encryptionKeys) // if a vault provider is missing a token, we will // gracefully fail here @@ -67,7 +64,6 @@ internal abstract class AbstractVaultSubmitter( // ========= USED FOR REPORTING IMPORTANT METRICS ========= val proxyResponseMonitor = VaultProxyResponseMonitor( - vault = vaultType, userAction = params.userAction, metricsLogger = logger ) @@ -78,7 +74,6 @@ internal abstract class AbstractVaultSubmitter( val vaultProxyRequest = buildProxyRequest( params = params, - encryptionKey = encryptionKey, vaultToken = vaultToken ).setPath(params.path).setParams(params) @@ -99,17 +94,14 @@ internal abstract class AbstractVaultSubmitter( } // abstract methods - internal abstract fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String internal abstract suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse internal abstract fun getVaultToken(paymentMethod: PaymentMethod): String? // concrete methods protected open fun buildProxyRequest( params: VaultSubmitterParams, - encryptionKey: String, vaultToken: String ) = VaultProxyRequest.emptyRequest() - .setHeader(ForageConstants.Headers.X_KEY, encryptionKey) .setHeader(ForageConstants.Headers.MERCHANT_ACCOUNT, params.merchantId) .setHeader(ForageConstants.Headers.IDEMPOTENCY_KEY, params.idempotencyKey) .setHeader(ForageConstants.Headers.TRACE_ID, logger.getTraceIdValue()) diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CapturePaymentRepository.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CapturePaymentRepository.kt index a2003098..3728d617 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CapturePaymentRepository.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CapturePaymentRepository.kt @@ -1,7 +1,5 @@ package com.joinforage.forage.android.core.services.vault -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeyService -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.payment.Payment import com.joinforage.forage.android.core.services.forageapi.payment.PaymentService @@ -12,7 +10,6 @@ import com.joinforage.forage.android.core.services.telemetry.UserAction internal class CapturePaymentRepository( private val vaultSubmitter: VaultSubmitter, - private val encryptionKeyService: EncryptionKeyService, private val paymentService: PaymentService, private val paymentMethodService: PaymentMethodService, private val logger: Log @@ -22,10 +19,6 @@ internal class CapturePaymentRepository( paymentRef: String, sessionToken: String ): ForageApiResponse { - val encryptionKeys = when (val response = encryptionKeyService.getEncryptionKey()) { - is ForageApiResponse.Success -> EncryptionKeys.ModelMapper.from(response.data) - else -> return response - } val paymentMethodRef = when (val response = paymentService.getPayment(paymentRef)) { is ForageApiResponse.Success -> Payment.getPaymentMethodRef(response.data) else -> return response @@ -37,7 +30,6 @@ internal class CapturePaymentRepository( return vaultSubmitter.submit( params = VaultSubmitterParams( - encryptionKeys = encryptionKeys, idempotencyKey = paymentRef, merchantId = merchantId, path = AbstractVaultSubmitter.capturePaymentPath(paymentRef), diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CheckBalanceRepository.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CheckBalanceRepository.kt index bbfc0735..56f6830a 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CheckBalanceRepository.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/CheckBalanceRepository.kt @@ -1,7 +1,5 @@ package com.joinforage.forage.android.core.services.vault -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeyService -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.paymentmethod.EbtBalance import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod @@ -12,7 +10,6 @@ import java.util.UUID internal class CheckBalanceRepository( private val vaultSubmitter: VaultSubmitter, - private val encryptionKeyService: EncryptionKeyService, private val paymentMethodService: PaymentMethodService, private val logger: Log ) { @@ -20,26 +17,21 @@ internal class CheckBalanceRepository( merchantId: String, sessionToken: String, paymentMethodRef: String, - getVaultRequestParams: ((EncryptionKeys, PaymentMethod) -> VaultSubmitterParams) = { encryptionKeys, paymentMethod -> + getVaultRequestParams: ((PaymentMethod) -> VaultSubmitterParams) = { paymentMethod -> buildVaultRequestParams( merchantId = merchantId, - encryptionKeys = encryptionKeys, paymentMethod = paymentMethod, sessionToken = sessionToken ) } ): ForageApiResponse { - val encryptionKeys = when (val response = encryptionKeyService.getEncryptionKey()) { - is ForageApiResponse.Success -> EncryptionKeys.ModelMapper.from(response.data) - else -> return response - } val paymentMethod = when (val response = paymentMethodService.getPaymentMethod(paymentMethodRef)) { is ForageApiResponse.Success -> PaymentMethod(response.data) else -> return response } val response = vaultSubmitter.submit( - getVaultRequestParams(encryptionKeys, paymentMethod) + getVaultRequestParams(paymentMethod) ) return if (response is ForageApiResponse.Success) { // response comes as (snap, non_snap) but we've historically @@ -59,12 +51,10 @@ internal class CheckBalanceRepository( private fun buildVaultRequestParams( merchantId: String, - encryptionKeys: EncryptionKeys, paymentMethod: PaymentMethod, sessionToken: String ): VaultSubmitterParams { return VaultSubmitterParams( - encryptionKeys = encryptionKeys, idempotencyKey = UUID.randomUUID().toString(), merchantId = merchantId, path = AbstractVaultSubmitter.balancePath(paymentMethod.ref), diff --git a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/DeferPaymentCaptureRepository.kt b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/DeferPaymentCaptureRepository.kt index 13fb9c6f..6cb8a496 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/DeferPaymentCaptureRepository.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/core/services/vault/DeferPaymentCaptureRepository.kt @@ -1,7 +1,5 @@ package com.joinforage.forage.android.core.services.vault -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeyService -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.payment.Payment import com.joinforage.forage.android.core.services.forageapi.payment.PaymentService @@ -12,7 +10,6 @@ import java.util.UUID internal class DeferPaymentCaptureRepository( private val vaultSubmitter: VaultSubmitter, - private val encryptionKeyService: EncryptionKeyService, private val paymentMethodService: PaymentMethodService, private val paymentService: PaymentService ) { @@ -24,10 +21,6 @@ internal class DeferPaymentCaptureRepository( paymentRef: String, sessionToken: String ): ForageApiResponse { - val encryptionKeys = when (val response = encryptionKeyService.getEncryptionKey()) { - is ForageApiResponse.Success -> EncryptionKeys.ModelMapper.from(response.data) - else -> return response - } val paymentMethodRef = when (val response = paymentService.getPayment(paymentRef)) { is ForageApiResponse.Success -> Payment.getPaymentMethodRef(response.data) else -> return response @@ -39,7 +32,6 @@ internal class DeferPaymentCaptureRepository( return vaultSubmitter.submit( VaultSubmitterParams( - encryptionKeys = encryptionKeys, idempotencyKey = UUID.randomUUID().toString(), merchantId = merchantId, path = AbstractVaultSubmitter.deferPaymentCapturePath(paymentRef), diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/ForageSDK.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/ForageSDK.kt index 5425943d..ce2adba2 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/ForageSDK.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/ForageSDK.kt @@ -3,7 +3,6 @@ package com.joinforage.forage.android.ecom.services import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.ForageConfig import com.joinforage.forage.android.core.services.ForageConfigNotSetException -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeyService import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.network.OkHttpClientBuilder import com.joinforage.forage.android.core.services.forageapi.payment.PaymentService @@ -208,7 +207,6 @@ class ForageSDK { // This block is used for Metrics Tracking! // ------------------------------------------------------ val measurement = CustomerPerceivedResponseMonitor( - vault = foragePinEditText.vault.vaultType, userAction = UserAction.BALANCE, logger ) @@ -304,7 +302,6 @@ class ForageSDK { // This block is used for Metrics Tracking! // ------------------------------------------------------ val measurement = CustomerPerceivedResponseMonitor( - vault = foragePinEditText.getVaultType(), userAction = UserAction.CAPTURE, logger ) @@ -423,7 +420,6 @@ class ForageSDK { traceId = logger.getTraceIdValue() ) } - private val encryptionKeyService by lazy { createEncryptionKeyService() } private val paymentMethodService by lazy { createPaymentMethodService() } private val paymentService by lazy { createPaymentService() } @@ -436,7 +432,6 @@ class ForageSDK { open fun createCheckBalanceRepository(foragePinEditText: ForagePINEditText): CheckBalanceRepository { return CheckBalanceRepository( vaultSubmitter = foragePinEditText.getVaultSubmitter(foragePinEditText.getForageConfig()!!.envConfig, logger), - encryptionKeyService = encryptionKeyService, paymentMethodService = paymentMethodService, logger = logger ) @@ -445,7 +440,6 @@ class ForageSDK { open fun createCapturePaymentRepository(foragePinEditText: ForagePINEditText): CapturePaymentRepository { return CapturePaymentRepository( vaultSubmitter = foragePinEditText.getVaultSubmitter(foragePinEditText.getForageConfig()!!.envConfig, logger), - encryptionKeyService = encryptionKeyService, paymentService = paymentService, paymentMethodService = paymentMethodService, logger = logger @@ -455,13 +449,11 @@ class ForageSDK { open fun createDeferPaymentCaptureRepository(foragePinEditText: ForagePINEditText): DeferPaymentCaptureRepository { return DeferPaymentCaptureRepository( vaultSubmitter = foragePinEditText.getVaultSubmitter(foragePinEditText.getForageConfig()!!.envConfig, logger), - encryptionKeyService = encryptionKeyService, paymentService = paymentService, paymentMethodService = paymentMethodService ) } - private fun createEncryptionKeyService() = EncryptionKeyService(config.apiBaseUrl, okHttpClient, logger) private fun createPaymentMethodService() = PaymentMethodService(config.apiBaseUrl, okHttpClient, logger) private fun createPaymentService() = PaymentService(config.apiBaseUrl, okHttpClient, logger) } diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/launchdarkly/LDManager.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/launchdarkly/LDManager.kt deleted file mode 100644 index 0559dd12..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/launchdarkly/LDManager.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.joinforage.forage.android.ecom.services.launchdarkly - -import android.app.Application -import com.joinforage.forage.android.core.services.VaultType -import com.joinforage.forage.android.core.services.telemetry.Log -import com.launchdarkly.sdk.ContextKind -import com.launchdarkly.sdk.LDContext -import com.launchdarkly.sdk.LDValue -import com.launchdarkly.sdk.android.LDClient -import com.launchdarkly.sdk.android.LDConfig - -internal object LDFlags { - const val ISO_POLLING_WAIT_INTERVALS = "iso-polling-wait-intervals" - const val ROSETTA_TRAFFIC_PERCENTAGE = "rosetta-traffic-percentage" -} - -internal object LDContexts { - const val ANDROID_CONTEXT = "android-sdk-service" -} - -internal object LDContextKind { - const val SERVICE = "service" -} - -// rosetta-traffic-percentage -internal val ALWAYS_ROSETTA_PERCENT = 100.0 -internal val ALWAYS_THIRD_PARTY_PERCENT = 0.0 - -internal fun computeVaultType(rosettaPercentage: Double): VaultType { - val randomNum = Math.random() * 100 - return if (randomNum <= rosettaPercentage) VaultType.FORAGE_VAULT_TYPE else VaultType.BT_VAULT_TYPE -} - -internal object LDManager { - // as much as I would LOVE to not have this be a shared state on a - // singleton object, Launch Darkly literally tells us to make it - // a singleton :/ - // - // https://docs.launchdarkly.com/sdk/client-side/android#:~:text=LDClient%20must%20be%20a%20singleton - private var client: LDClient? = null - - internal fun initialize(app: Application, ldConfig: LDConfig) { - val contextKind = ContextKind.of(LDContextKind.SERVICE) - val context = LDContext.create(contextKind, LDContexts.ANDROID_CONTEXT) - client = LDClient.init(app, ldConfig, context, 1) - } - - internal fun getVaultProvider(logger: Log = Log.getSilentInstance()): VaultType { - val rosettaPercent = client?.doubleVariation( - LDFlags.ROSETTA_TRAFFIC_PERCENTAGE, - ALWAYS_ROSETTA_PERCENT - ) ?: ALWAYS_ROSETTA_PERCENT - logger.i("[LaunchDarkly] Rosetta percent of $rosettaPercent returned from LD") - - // convert the rosetta flag percent into an answer to which vault provider to use - val vaultType = computeVaultType(rosettaPercent) - logger.i("[LaunchDarkly] Vault type set to $vaultType") - - // return vault provider - return vaultType - } - - internal fun getPollingIntervals(logger: Log = Log.getSilentInstance()): LongArray { - val defaultVal = LDValue.buildObject().put( - "intervals", - LDValue.Convert.Long.arrayFrom(List(10) { 1000L }) - ).build() - - val jsonIntervals = client?.jsonValueVariation( - LDFlags.ISO_POLLING_WAIT_INTERVALS, - defaultVal - ) ?: defaultVal - - val intervals = jsonIntervals.get("intervals") - - // Converting the LDArray into a LongArray - val pollingList = LongArray(intervals.size()) { intervals.get(it).longValue() } - - logger.i("[LaunchDarkly] polling intervals $pollingList") - return pollingList - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BTResponseParser.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BTResponseParser.kt deleted file mode 100644 index d465569b..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BTResponseParser.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.bt - -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.UnknownErrorApiResponse -import com.joinforage.forage.android.core.services.vault.VaultResponseParser -import org.json.JSONObject - -class UnknownBTSuccessResponse(response: Any?) : Exception(response.toString()) - -internal class BTResponseParser(btRes: Result) : VaultResponseParser { - override val isNullResponse: Boolean = false - override val vaultErrorMsg: String? = btRes.exceptionOrNull()?.message - override val rawResponse: String = btRes.toString() - - override val vaultError: ForageApiResponse.Failure? - override val forageError: ForageApiResponse.Failure? - override val successfulResponse: ForageApiResponse.Success? - - val isSuccessful: Boolean - - private val failureResponseRegExp = BtFailureResponseRegExp(btRes) - - init { - // BT returns a Result so it's never null - vaultError = parseVaultError(btRes) - forageError = parseForageError(failureResponseRegExp) - isSuccessful = btRes.isSuccess && vaultError == null && forageError == null - successfulResponse = parseSuccessfulResponse(btRes) - } - - private fun parseVaultError(vaultResponse: BasisTheoryResponse): ForageApiResponse.Failure? { - if (vaultResponse.isSuccess) return null - // In AbstractVaultSubmitter we track the forageError - // and the forage status code of the UnknownErrorApiResponse - // via the VaultProxyResponseMonitor. This keeps us informed - // of *when* erroroneous BT response happen. Unfortunately, - // we do not currently track the specifics of the proxy_error - // that that BT returned to us. - // TODO: log the specific error that BT responds with when - // resRegExp.containsProxyError == true - return if (failureResponseRegExp.containsProxyError) UnknownErrorApiResponse else null - } - - private fun parseForageError(resRegExp: BtFailureResponseRegExp): ForageApiResponse.Failure? = - if (resRegExp.bodyText == null || - resRegExp.statusCode == null || - // if there's a proxy_error, don't try to parse as a Forage error - resRegExp.containsProxyError - ) { - null - } else { - // if there's a body, a status code, and no proxy_error, then - // we embrace that the following line will throw an exception - ForageApiResponse.Failure(resRegExp.statusCode, resRegExp.bodyText) - } - - private fun parseSuccessfulResponse(vaultResponse: Result): ForageApiResponse.Success? { - return if (!isSuccessful) { - null - } else { - // Basis Theory appears to leak their Gson dependency in - // the response as the value of Any? resolves to a - // com.google.gson.internal.LinkedTreeMap instance. - // Fortunately, this is of type Map so we can - // still parse it with JSONObject and avoid having to - // directory depend on Gson ourselves - val result = vaultResponse.getOrThrow() - val jsonResponse = when { - (result == null) -> JSONObject() - (result == "") -> JSONObject() - (result is Map<*, *>) -> JSONObject(result) - (result is String) -> JSONObject(result) - else -> throw UnknownBTSuccessResponse(result) - } - ForageApiResponse.Success(jsonResponse.toString()) - } - } -} 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 deleted file mode 100644 index 520b1461..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BasisTheoryPinSubmitter.kt +++ /dev/null @@ -1,89 +0,0 @@ -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 -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.UnknownErrorApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.error.payload.UnknownForageFailureResponse -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.SecurePinCollector -import com.joinforage.forage.android.core.services.vault.VaultProxyRequest -import com.joinforage.forage.android.core.services.vault.VaultSubmitterParams - -internal typealias BasisTheoryResponse = Result - -internal class BasisTheoryPinSubmitter( - private val btTextElement: TextElement, - collector: SecurePinCollector, - private val envConfig: EnvConfig, - logger: Log, - private val buildVaultProvider: () -> BasisTheoryElements = { buildBasisTheory(envConfig) } -) : AbstractVaultSubmitter( - collector = collector, - logger = logger -) { - override val vaultType: VaultType = VaultType.BT_VAULT_TYPE - override fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String = encryptionKeys.btAlias - - // Basis Theory requires a few extra headers beyond the - // common headers to make proxy requests - override fun buildProxyRequest( - params: VaultSubmitterParams, - encryptionKey: String, - vaultToken: String - ) = super - .buildProxyRequest( - params = params, - encryptionKey = encryptionKey, - vaultToken = vaultToken - ) - .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 proxyRequest: ProxyRequest = ProxyRequest().apply { - headers = vaultProxyRequest.headers + (ForageConstants.Headers.API_VERSION to "2024-01-08") - body = ProxyRequestObject( - pin = btTextElement, - card_number_token = vaultProxyRequest.vaultToken - ) - path = vaultProxyRequest.path - } - - val vaultResponse = runCatching { - bt.proxy.post(proxyRequest) - } - - return try { - vaultToForageResponse(BTResponseParser(vaultResponse)) - } catch (e: UnknownBTSuccessResponse) { - logger.e("Unknown success response from BasisTheory Vault.", e) - UnknownErrorApiResponse - } catch (e: UnknownForageFailureResponse) { - logger.e("Unknown error from Payments API.", e) - UnknownErrorApiResponse - } catch (e: Exception) { - logger.e("Request to Basis Theory failed.", e) - UnknownErrorApiResponse - } - } - - override fun getVaultToken(paymentMethod: PaymentMethod): String? = pickVaultTokenByIndex(paymentMethod, 1) - - companion object { - private fun buildBasisTheory(envConfig: EnvConfig): BasisTheoryElements { - return BasisTheoryElements.builder() - .apiKey(envConfig.btAPIKey) - .build() - } - } -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BtFailureResponseRegExp.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BtFailureResponseRegExp.kt deleted file mode 100644 index 7764e09c..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/BtFailureResponseRegExp.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.bt - -internal class BtFailureResponseRegExp(res: Result) { - private val bodyRegex = ("HTTP response body: (.+?)\\n".toRegex()) - private val statusCodeRegex = "HTTP response code: (\\d+)".toRegex() - - val bodyText: String? - val statusCode: Int? - - val containsProxyError: Boolean - - init { - // NOTE: The response will either be a BT original response - // or if there was no problem with BT, the response can also - // be a Forage original response (e.g. a success or an error) - // For this reason, `ResponseRegExp` is used to parse - val message = res.exceptionOrNull()?.message.toString() - bodyText = bodyRegex.find(message)?.groupValues?.get(1) - statusCode = statusCodeRegex.find(message)?.groupValues?.get(1)?.toIntOrNull() - - // com.basistheory.ApiException isn't currently - // publicly-exposed by the basis-theory-android package - // so we parse the raw exception message to retrieve the body of the BasisTheory - // errors - containsProxyError = bodyText?.contains("proxy_error") ?: false - } -} -// TODO: write unit tests for this class....is it actually possible for .get(1) to throw??? diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/ProxyRequestObject.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/ProxyRequestObject.kt deleted file mode 100644 index 2fc7f8b2..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/bt/ProxyRequestObject.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.bt - -import com.basistheory.android.view.TextElement - -/** - * Body of the proxy request to Basis Theory - * IMPORTANT: Any changes to the [ProxyRequestObject] must be reflected in the consumer-rules.pro file - */ -internal data class ProxyRequestObject(val pin: TextElement, val card_number_token: String) 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 7c219c83..08bd40d1 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 @@ -6,7 +6,6 @@ import com.joinforage.forage.android.core.services.ForageConstants import com.joinforage.forage.android.core.services.VaultType import com.joinforage.forage.android.core.services.addPathSegmentsSafe import com.joinforage.forage.android.core.services.addTrailingSlash -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse import com.joinforage.forage.android.core.services.forageapi.network.NetworkService import com.joinforage.forage.android.core.services.forageapi.network.OkHttpClientBuilder @@ -37,11 +36,6 @@ internal class RosettaPinSubmitter( ) { override val vaultType: VaultType = VaultType.FORAGE_VAULT_TYPE - // x-key header is not applicable to forage - override fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String { - return "" - } - override suspend fun submitProxyRequest(vaultProxyRequest: VaultProxyRequest): ForageApiResponse { return try { val apiUrl = vaultUrlBuilder(vaultProxyRequest.path) @@ -75,12 +69,10 @@ internal class RosettaPinSubmitter( override fun buildProxyRequest( params: VaultSubmitterParams, - encryptionKey: String, vaultToken: String ) = super .buildProxyRequest( params = params, - encryptionKey = encryptionKey, vaultToken = vaultToken ) .setHeader(ForageConstants.Headers.CONTENT_TYPE, "application/json") diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaResponseParser.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaResponseParser.kt index c386a733..00a00203 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaResponseParser.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/forage/RosettaResponseParser.kt @@ -6,7 +6,7 @@ import com.joinforage.forage.android.core.services.vault.VaultResponseParser internal class RosettaResponseParser(rosettaResponse: ForageApiResponse) : VaultResponseParser { override val isNullResponse: Boolean = false - // Unlike VGS and Basis Theory, Vault-specific errors are handled in + // Vault-specific errors are handled in // the try..catch block that makes the request to the vault, so we // just return null here. override val vaultError: ForageApiResponse.Failure? = null diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VGSResponseParser.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VGSResponseParser.kt deleted file mode 100644 index 40c1a25b..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VGSResponseParser.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.vgs - -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.UnknownErrorApiResponse -import com.joinforage.forage.android.core.services.vault.VaultResponseParser -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import org.json.JSONException - -internal class VGSResponseParser(vgsRes: VGSResponse?) : VaultResponseParser { - override val isNullResponse: Boolean - override val vaultError: ForageApiResponse.Failure? - override val forageError: ForageApiResponse.Failure? - override val successfulResponse: ForageApiResponse.Success? - - override val vaultErrorMsg: String = vgsRes?.body.toString() - override val rawResponse: String = vgsRes.toString() - - init { - if (vgsRes == null) { - isNullResponse = true - vaultError = null - forageError = null - successfulResponse = null - } else { - isNullResponse = false - vaultError = parseVaultError(vgsRes) - forageError = parseForageError(vgsRes) - successfulResponse = parseSuccessfulResponse(vgsRes) - } - } - - private fun parseVaultError(res: VGSResponse): ForageApiResponse.Failure? { - if (res is VGSResponse.SuccessResponse) return null - - // given that it's not a success and it's not a forage error - // that only leaves a VGS error. - // TODO: investigate how to get identify the actual VGS error - // for logging purposes - return if (parseForageError(res) == null) UnknownErrorApiResponse else null - } - - private fun parseForageError(res: VGSResponse): ForageApiResponse.Failure? { - if (res is VGSResponse.SuccessResponse) return null - - val errorRes = res as VGSResponse.ErrorResponse - return try { - // if the response is a ForageApiError, then this block - // should not throw - ForageApiResponse.Failure(errorRes.errorCode, errorRes.body ?: "") - } catch (_: JSONException) { - // if we throw when trying to extract the ForageApiError, - // that means the response is not a ForageApiError - null - } - } - - private fun parseSuccessfulResponse(res: VGSResponse): ForageApiResponse.Success? { - return when (res) { - is VGSResponse.ErrorResponse -> null - is VGSResponse.SuccessResponse -> - ForageApiResponse.Success(res.body.toString()) - } - } -} 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 deleted file mode 100644 index abe6ae1f..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/services/vault/vgs/VgsPinSubmitter.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.vgs - -import com.joinforage.forage.android.core.services.EnvConfig -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 -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.SecurePinCollector -import com.joinforage.forage.android.core.services.vault.VaultProxyRequest -import com.verygoodsecurity.vgscollect.VGSCollectLogger -import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.VGSCollect -import com.verygoodsecurity.vgscollect.core.VgsCollectResponseListener -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import com.verygoodsecurity.vgscollect.widget.VGSEditText -import kotlin.coroutines.suspendCoroutine - -internal class VgsPinSubmitter( - private val vgsEditText: VGSEditText, - collector: SecurePinCollector, - private val envConfig: EnvConfig, - logger: Log -) : AbstractVaultSubmitter(collector, logger) { - override val vaultType: VaultType = VaultType.VGS_VAULT_TYPE - override suspend fun submitProxyRequest( - vaultProxyRequest: VaultProxyRequest - ): ForageApiResponse = suspendCoroutine { continuation -> - VGSCollectLogger.isEnabled = false - val vgsCollect = VGSCollect - .Builder(vgsEditText.context, envConfig.vgsVaultId) - .setEnvironment(envConfig.vgsVaultType) - .create() - vgsCollect.bindView(vgsEditText) - - vgsCollect.addOnResponseListeners(object : VgsCollectResponseListener { - override fun onResponse(response: VGSResponse?) { - // Clear all information collected before by VGSCollect - vgsCollect.onDestroy() - - continuation.resumeWith( - Result.success(vaultToForageResponse(VGSResponseParser(response))) - ) - } - }) - - val request: VGSRequest = VGSRequest.VGSRequestBuilder() - .setMethod(HTTPMethod.POST) - .setPath(vaultProxyRequest.path) - .setCustomHeader(vaultProxyRequest.headers) - .setCustomData(buildBaseRequestBody(vaultProxyRequest)) - .build() - - vgsCollect.asyncSubmit(request) - } - - override fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String { - return encryptionKeys.vgsAlias - } - - override fun getVaultToken(paymentMethod: PaymentMethod): String? = - pickVaultTokenByIndex(paymentMethod, 0) -} diff --git a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/element/ForagePINEditText.kt b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/element/ForagePINEditText.kt index 3dee7d36..da9884ad 100644 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/element/ForagePINEditText.kt +++ b/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/element/ForagePINEditText.kt @@ -1,25 +1,18 @@ package com.joinforage.forage.android.ecom.ui.element -import android.app.Application import android.content.Context import android.graphics.Typeface import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatEditText import com.joinforage.forage.android.R -import com.joinforage.forage.android.core.services.EnvConfig import com.joinforage.forage.android.core.services.ForageConfig import com.joinforage.forage.android.core.services.ForageConfigNotSetException -import com.joinforage.forage.android.core.services.VaultType import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.ui.VaultWrapper import com.joinforage.forage.android.core.ui.element.DynamicEnvElement import com.joinforage.forage.android.core.ui.element.ForageConfigManager import com.joinforage.forage.android.core.ui.element.ForagePinElement import com.joinforage.forage.android.core.ui.getLogoImageViewLayout -import com.joinforage.forage.android.ecom.services.launchdarkly.LDManager -import com.joinforage.forage.android.ecom.ui.vault.bt.BTVaultWrapper import com.joinforage.forage.android.ecom.ui.vault.rosetta.RosettaPinElement -import com.launchdarkly.sdk.android.LDConfig /** * A [ForageElement][com.joinforage.forage.android.core.ui.element.ForageElement] that securely collects a card PIN. You need a [ForagePINEditText] to call @@ -57,7 +50,6 @@ class ForagePINEditText @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.foragePanEditTextStyle ) : ForagePinElement(context, attrs, defStyleAttr), DynamicEnvElement { - private val btVaultWrapper: BTVaultWrapper private val rosettaPinElement: RosettaPinElement /** @@ -91,49 +83,15 @@ class ForagePINEditText @JvmOverloads constructor( context.obtainStyledAttributes(attrs, R.styleable.ForagePINEditText, defStyleAttr, 0) .apply { try { - - // at this point in time, we do not know the environment and - // we are operating and thus do not know whether to add - // BTVaultWrapper or ForageVaultElement to the UI. - // But that's OK. We can hedge and instantiate all of them. - // Then, within setForageConfig, once we know the environment - // and are thus able to initial LaunchDarkly and find out - // whether to use BT or Forage. So, below we are hedging. - btVaultWrapper = BTVaultWrapper(context, attrs, defStyleAttr) rosettaPinElement = RosettaPinElement(context, attrs, defStyleAttr) - // ensure all wrappers init with the - // same typeface (or the attributes) - btVaultWrapper.typeface = rosettaPinElement.typeface + val textSize = getDimension(R.styleable.ForagePINEditText_textSize, -1f) + if (textSize != -1f) { + rosettaPinElement.setTextSize(textSize) + } } finally { recycle() } } - - // The following pieces of code are to fix height - // differences in the appearance of Rosetta-backed - // vs BT-backed ForagePINEditText in the case where - // no app:inputHeight or app:inputWidth are set. - - // zero out the padding for Basis Theory element - val btFrame = btVaultWrapper.getTextElement() - val btTextElement = btFrame.getChildAt(0) as AppCompatEditText - btTextElement.setPadding(0, 0, 0, 0) - - // ensure Rosetta's textSize is the same as BTs textSize - // There are three cases: - // 1) using XML layouts and somebody passes app:textSize -> - // both RosettaPinElement and BTVaultWrapper read - // that value and independently set the correct textSize - // 2) using XML layouts and app:textSize is not set -> - // This line of code fixes that issue - // 3) create dynamic instance of ForagePINEditText and - // call setTextSize -> - // setTextSize calls vault.setTextSize so the only - // visible text field will have the correct textSize - // 3) create dynamic instance of ForagePINEditText and - // never call setTextSize -> - // This line of code fixes that issue - rosettaPinElement.setTextSize(btTextElement.textSize) } private fun initWithForageConfig(forageConfig: ForageConfig) { @@ -143,11 +101,7 @@ class ForagePINEditText @JvmOverloads constructor( val logger = Log.getInstance() logger.initializeDD(context, forageConfig) - // defer to the concrete subclass for the details of obtaining - // and instantiating the backing vault instance. We just need - // to guarantee that the vault is instantiated prior to adding - // it to the parent view - _SET_ONLY_vault = determineBackingVault(forageConfig, logger) + _SET_ONLY_vault = rosettaPinElement _linearLayout.addView(vault.getTextElement()) _linearLayout.addView(getLogoImageViewLayout(context)) @@ -183,29 +137,11 @@ class ForagePINEditText @JvmOverloads constructor( forageConfigManager.forageConfig = forageConfig } - private fun determineBackingVault(forageConfig: ForageConfig, logger: Log): VaultWrapper { - // initialize Launch Darkly singleton - val ldMobileKey = EnvConfig.fromForageConfig(forageConfig).ldMobileKey - val ldConfig = LDConfig.Builder().mobileKey(ldMobileKey).build() - LDManager.initialize(context.applicationContext as Application, ldConfig) - - // decide on a vault provider and the corresponding vault wrapper - val vaultType = LDManager.getVaultProvider(logger) - return if (vaultType == VaultType.BT_VAULT_TYPE) { - btVaultWrapper - } else { - rosettaPinElement - } - } - internal fun getForageConfig() = forageConfigManager.forageConfig override var typeface: Typeface? - get() = if (vault == btVaultWrapper) btVaultWrapper.typeface else rosettaPinElement.typeface + get() = rosettaPinElement.typeface set(value) { - // keep all vault providers in sync regardless of - // whether they were added to the UI - btVaultWrapper.typeface = value rosettaPinElement.typeface = value } } 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 deleted file mode 100644 index 21e22c5e..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/bt/BTVaultWrapper.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.joinforage.forage.android.ecom.ui.vault.bt - -import android.content.Context -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.util.AttributeSet -import android.view.Gravity -import android.view.ViewGroup -import android.widget.LinearLayout -import com.basistheory.android.view.TextElement -import com.basistheory.android.view.mask.ElementMask -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.getBoxCornerRadiusBottomEnd -import com.joinforage.forage.android.core.ui.getBoxCornerRadiusBottomStart -import com.joinforage.forage.android.core.ui.getBoxCornerRadiusTopEnd -import com.joinforage.forage.android.core.ui.getBoxCornerRadiusTopStart -import com.joinforage.forage.android.ecom.services.vault.bt.BasisTheoryPinSubmitter - -internal class BTVaultWrapper @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : VaultWrapper(context, attrs, defStyleAttr) { - private var _internalTextElement: TextElement - override val vaultType: VaultType = VaultType.BT_VAULT_TYPE - - init { - context.obtainStyledAttributes(attrs, com.joinforage.forage.android.R.styleable.ForagePINEditText, defStyleAttr, 0) - .apply { - val textInputLayoutStyleAttribute = - getResourceId(com.joinforage.forage.android.R.styleable.ForagePINEditText_pinInputLayoutStyle, 0) - val boxStrokeColor = getColor( - com.joinforage.forage.android.R.styleable.ForagePINEditText_pinBoxStrokeColor, - getThemeAccentColor(context) - ) - val boxBackgroundColor = getColor(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxBackgroundColor, Color.TRANSPARENT) - val defRadius = resources.getDimension(com.joinforage.forage.android.R.dimen.default_horizontal_field) - val boxCornerRadius = - getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxCornerRadius, defRadius) - val boxCornerRadiusTopStart = getBoxCornerRadiusTopStart(boxCornerRadius) - val boxCornerRadiusTopEnd = getBoxCornerRadiusTopEnd(boxCornerRadius) - val boxCornerRadiusBottomStart = getBoxCornerRadiusBottomStart(boxCornerRadius) - val boxCornerRadiusBottomEnd = getBoxCornerRadiusBottomEnd(boxCornerRadius) - val hintTextColorVal = - getColor(com.joinforage.forage.android.R.styleable.ForagePINEditText_hintTextColor, getThemeAccentColor(context)) - - val inputWidth: Int = getDimensionPixelSize(com.joinforage.forage.android.R.styleable.ForagePINEditText_inputWidth, ViewGroup.LayoutParams.MATCH_PARENT) - val inputHeight: Int = getDimensionPixelSize(com.joinforage.forage.android.R.styleable.ForagePINEditText_inputHeight, ViewGroup.LayoutParams.WRAP_CONTENT) - - try { - _internalTextElement = TextElement(context, null, textInputLayoutStyleAttribute).apply { - layoutParams = - LinearLayout.LayoutParams( - inputWidth, - inputHeight - ) - inputType = com.basistheory.android.model.InputType.NUMBER_PASSWORD - val digit = Regex("""\d""") - mask = ElementMask(listOf(digit, digit, digit, digit)) - hint = getString(com.joinforage.forage.android.R.styleable.ForagePINEditText_hint) - hintTextColor = hintTextColorVal - textSize = getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_textSize, -1f) - textColor = getColor(com.joinforage.forage.android.R.styleable.ForagePINEditText_textColor, Color.BLACK) - var customBackground = GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - cornerRadii = floatArrayOf(boxCornerRadiusTopStart, boxCornerRadiusTopStart, boxCornerRadiusTopEnd, boxCornerRadiusTopEnd, boxCornerRadiusBottomStart, boxCornerRadiusBottomStart, boxCornerRadiusBottomEnd, boxCornerRadiusBottomEnd) - setStroke(5, boxStrokeColor) - setColor(boxBackgroundColor) - } - - background = customBackground - gravity = Gravity.CENTER - } - - // Basis Theory keeps a list of as many listeners as you want - // for a given event see here: https://tinyurl.com/yc6wzt8v - // We only support attaching a single listener - // to any event for simplicity. In order work with BT's - // append-only subscribe protocol, we will only ever subscribe - // a single listener to Basis Theory during initialization - // and we will use a mutating reference that only points - // the most recent event listener - _internalTextElement.addFocusEventListener { - focusState = focusState.focus() - focusState.fireEvent( - onFocusEventListener = onFocusEventListener, - onBlurEventListener = onBlurEventListener - ) - } - _internalTextElement.addBlurEventListener { - focusState = focusState.blur() - focusState.fireEvent( - onFocusEventListener = onFocusEventListener, - onBlurEventListener = onBlurEventListener - ) - } - _internalTextElement.addChangeEventListener { state -> - // map Basis Theory's event representation to Forage's - inputState = inputState.handleChangeEvent( - isComplete = state.isComplete, - isEmpty = state.isEmpty - ) - onChangeEventListener?.invoke(pinEditTextState) - } - } finally { - recycle() - } - } - } - - override fun clearText() { - _internalTextElement.setText("") - } - - override fun getTextElement(): TextElement { - return _internalTextElement - } - - override fun getVaultSubmitter( - envConfig: EnvConfig, - logger: Log - ): AbstractVaultSubmitter = BasisTheoryPinSubmitter( - _internalTextElement, - object : SecurePinCollector { - override fun clearText() { - this@BTVaultWrapper.clearText() - } - override fun isComplete(): Boolean = inputState.isComplete - }, - envConfig, - logger - ) - - override fun showKeyboard() { - _internalTextElement.showKeyboard(0) - } - - override var typeface: Typeface? - get() = _internalTextElement.typeface - set(value) { - _internalTextElement.typeface = value - } - - override fun setTextColor(textColor: Int) { - _internalTextElement.textColor = textColor - } - - override fun setTextSize(textSize: Float) { - _internalTextElement.textSize = textSize - } - - @Deprecated( - message = "setHint (for *PIN* elements) is deprecated and will be removed in a future major release.", - level = DeprecationLevel.WARNING - ) - override fun setHint(hint: String) { - // no-op, deprecated! - } - - @Deprecated( - message = "setHintTextColor (for *PIN* elements) is deprecated and will be removed in a future major release.", - level = DeprecationLevel.WARNING - ) - override fun setHintTextColor(hintTextColor: Int) { - // no-op, deprecated! - } -} 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 deleted file mode 100644 index 135022d5..00000000 --- a/forage-android/src/main/java/com/joinforage/forage/android/ecom/ui/vault/vgs/VGSVaultWrapper.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.joinforage.forage.android.ecom.ui.vault.vgs - -import android.content.Context -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.util.AttributeSet -import android.util.TypedValue -import android.view.Gravity -import android.view.ViewGroup -import android.widget.LinearLayout -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.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.getBoxCornerRadiusBottomEnd -import com.joinforage.forage.android.core.ui.getBoxCornerRadiusBottomStart -import com.joinforage.forage.android.core.ui.getBoxCornerRadiusTopEnd -import com.joinforage.forage.android.core.ui.getBoxCornerRadiusTopStart -import com.joinforage.forage.android.ecom.services.vault.vgs.VgsPinSubmitter -import com.verygoodsecurity.vgscollect.core.model.state.FieldState -import com.verygoodsecurity.vgscollect.core.storage.OnFieldStateChangeListener -import com.verygoodsecurity.vgscollect.view.card.validation.rules.VGSInfoRule -import com.verygoodsecurity.vgscollect.widget.VGSEditText - -internal class VGSVaultWrapper @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : VaultWrapper(context, attrs, defStyleAttr) { - private var _internalEditText: VGSEditText - override val vaultType: VaultType = VaultType.VGS_VAULT_TYPE - - init { - context.obtainStyledAttributes(attrs, com.joinforage.forage.android.R.styleable.ForagePINEditText, defStyleAttr, 0) - .apply { - try { - val hint = getString(com.joinforage.forage.android.R.styleable.ForagePINEditText_hint) - val textInputLayoutStyleAttribute = - getResourceId(com.joinforage.forage.android.R.styleable.ForagePINEditText_pinInputLayoutStyle, 0) - val boxStrokeColor = getColor( - com.joinforage.forage.android.R.styleable.ForagePINEditText_pinBoxStrokeColor, - getThemeAccentColor(context) - ) - val boxBackgroundColor = getColor(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxBackgroundColor, Color.TRANSPARENT) - val defRadius = resources.getDimension(com.joinforage.forage.android.R.dimen.default_horizontal_field) - val boxCornerRadius = - getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_boxCornerRadius, defRadius) - val boxCornerRadiusTopStart = getBoxCornerRadiusTopStart(boxCornerRadius) - val boxCornerRadiusTopEnd = getBoxCornerRadiusTopEnd(boxCornerRadius) - val boxCornerRadiusBottomStart = getBoxCornerRadiusBottomStart(boxCornerRadius) - val boxCornerRadiusBottomEnd = getBoxCornerRadiusBottomEnd(boxCornerRadius) - val hintTextColor = - getColorStateList(com.joinforage.forage.android.R.styleable.ForagePINEditText_hintTextColor) - val textSize = getDimension(com.joinforage.forage.android.R.styleable.ForagePINEditText_textSize, -1f) - val textColor = getColor(com.joinforage.forage.android.R.styleable.ForagePINEditText_textColor, Color.BLACK) - - val inputWidth: Int = getDimensionPixelSize(com.joinforage.forage.android.R.styleable.ForagePINEditText_inputWidth, ViewGroup.LayoutParams.MATCH_PARENT) - val inputHeight: Int = getDimensionPixelSize(com.joinforage.forage.android.R.styleable.ForagePINEditText_inputHeight, ViewGroup.LayoutParams.WRAP_CONTENT) - - _internalEditText = VGSEditText(context, null, textInputLayoutStyleAttribute).apply { - layoutParams = - LinearLayout.LayoutParams( - inputWidth, - inputHeight - ) - - setHint(hint) - hintTextColor?.let { - setHintTextColor(hintTextColor) - } - - val customBackground = GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - cornerRadii = floatArrayOf(boxCornerRadiusTopStart, boxCornerRadiusTopStart, boxCornerRadiusTopEnd, boxCornerRadiusTopEnd, boxCornerRadiusBottomStart, boxCornerRadiusBottomStart, boxCornerRadiusBottomEnd, boxCornerRadiusBottomEnd) - setStroke(5, boxStrokeColor) - setColor(boxBackgroundColor) - } - background = customBackground - - setTextColor(textColor) - setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) - setGravity(Gravity.CENTER) - - // Constant VGS configuration. Can't be externally altered! - setFieldName(ForageConstants.VGS.PIN_FIELD_NAME) - setMaxLength(4) - setInputType(android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD) - } - // enforce that PINs must be 4 digits to be vali - _internalEditText.appendRule( - VGSInfoRule.ValidationBuilder() - .setRegex("\\d{4}") - .build() - ) - - // VGS works with the conventional setOnFocusChangeListener - // see https://tinyurl.com/2urct5er, which means a single - // listener handles the focus and blur logic. We split this - // up into separate focus and blur listeners. This requires - // that we pass a single listener to VGS on init that uses - // mutable references to listeners so that setting the focus - // would not remove the blur listener and vice versa - _internalEditText.setOnFocusChangeListener { _, hasFocus -> - focusState = focusState.changeFocus(hasFocus) - focusState.fireEvent( - onFocusEventListener = onFocusEventListener, - onBlurEventListener = onBlurEventListener - ) - } - _internalEditText.setOnFieldStateChangeListener(object : OnFieldStateChangeListener { - override fun onStateChange(state: FieldState) { - // map VGS's event representation to Forage's - inputState = inputState.handleChangeEvent( - isComplete = state.isValid, - isEmpty = state.isEmpty - ) - onChangeEventListener?.invoke(pinEditTextState) - } - }) - } finally { - recycle() - } - } - } - - override fun clearText() { - _internalEditText.setText("") - } - - override fun getTextElement(): VGSEditText = _internalEditText - - override fun getVaultSubmitter( - envConfig: EnvConfig, - logger: Log - ): AbstractVaultSubmitter = VgsPinSubmitter( - _internalEditText, - object : SecurePinCollector { - override fun clearText() { - this@VGSVaultWrapper.clearText() - } - override fun isComplete(): Boolean = inputState.isComplete - }, - envConfig, - logger - ) - - override fun showKeyboard() { - _internalEditText.showKeyboard() - } - - override var typeface: Typeface? - get() = _internalEditText.getTypeface() - set(value) { - if (value != null) { - _internalEditText.setTypeface(value) - } - } - - override fun setTextColor(textColor: Int) { - _internalEditText.setTextColor(textColor) - } - - override fun setTextSize(textSize: Float) { - _internalEditText.setTextSize(textSize) - } - - override fun setHint(hint: String) { - _internalEditText.setHint(hint) - } - - override fun setHintTextColor(hintTextColor: Int) { - _internalEditText.setHintTextColor(hintTextColor) - } -} diff --git a/forage-android/src/main/res/values/dimens.xml b/forage-android/src/main/res/values/dimens.xml index 4df4c258..9833432d 100644 --- a/forage-android/src/main/res/values/dimens.xml +++ b/forage-android/src/main/res/values/dimens.xml @@ -6,7 +6,5 @@ 0dp 0dp 0dp - 0dp - 0dp 0dp \ No newline at end of file diff --git a/forage-android/src/test/java/com/joinforage/forage/android/core/telemetry/metrics/ResponseMonitorTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/core/telemetry/metrics/ResponseMonitorTest.kt index 07da4bd3..55f33fa1 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/core/telemetry/metrics/ResponseMonitorTest.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/core/telemetry/metrics/ResponseMonitorTest.kt @@ -47,7 +47,7 @@ class ResponseMonitorTest { @Test fun `Vault proxy monitor should log error if path is not set`() { val mockLogger = MockLogger() - val vaultProxyResponseMonitor = VaultProxyResponseMonitor(vault = VaultType.VGS_VAULT_TYPE, userAction = UserAction.CAPTURE, mockLogger) + val vaultProxyResponseMonitor = VaultProxyResponseMonitor(userAction = UserAction.CAPTURE, mockLogger) vaultProxyResponseMonitor.setMethod("POST").setHttpStatusCode(200).logResult() Assertions.assertThat(mockLogger.errorLogs.count()).isEqualTo(1) Assertions.assertThat(mockLogger.infoLogs.count()).isEqualTo(0) @@ -57,7 +57,7 @@ class ResponseMonitorTest { @Test fun `Vault proxy monitor should log error if method is not set`() { val mockLogger = MockLogger() - val vaultProxyResponseMonitor = VaultProxyResponseMonitor(vault = VaultType.VGS_VAULT_TYPE, userAction = UserAction.CAPTURE, mockLogger) + val vaultProxyResponseMonitor = VaultProxyResponseMonitor(userAction = UserAction.CAPTURE, mockLogger) vaultProxyResponseMonitor.setPath("this/is/test/path/").setHttpStatusCode(200).logResult() Assertions.assertThat(mockLogger.errorLogs.count()).isEqualTo(1) Assertions.assertThat(mockLogger.infoLogs.count()).isEqualTo(0) @@ -67,7 +67,7 @@ class ResponseMonitorTest { @Test fun `Vault proxy monitor should log error if status code is not set`() { val mockLogger = MockLogger() - val vaultProxyResponseMonitor = VaultProxyResponseMonitor(vault = VaultType.VGS_VAULT_TYPE, userAction = UserAction.CAPTURE, mockLogger) + val vaultProxyResponseMonitor = VaultProxyResponseMonitor(userAction = UserAction.CAPTURE, mockLogger) vaultProxyResponseMonitor.setPath("this/is/test/path/").setMethod("POST").logResult() Assertions.assertThat(mockLogger.errorLogs.count()).isEqualTo(1) Assertions.assertThat(mockLogger.infoLogs.count()).isEqualTo(0) @@ -77,9 +77,9 @@ class ResponseMonitorTest { @Test fun `Validate the attributes of a successful vault proxy log`() { val mockLogger = MockLogger() - val vaultType = VaultType.VGS_VAULT_TYPE + val vaultType = VaultType.FORAGE_VAULT_TYPE val userAction = UserAction.CAPTURE - val vaultProxyResponseMonitor = VaultProxyResponseMonitor(vault = vaultType, userAction = userAction, mockLogger) + val vaultProxyResponseMonitor = VaultProxyResponseMonitor(userAction = userAction, mockLogger) val path = "this/is/test/path/" val method = "POST" val statusCode = 200 @@ -113,7 +113,7 @@ class ResponseMonitorTest { @Test fun `Customer perceived monitor should log error if outcome type is not set`() { val mockLogger = MockLogger() - val customerPerceivedResponseMonitor = CustomerPerceivedResponseMonitor(vault = VaultType.VGS_VAULT_TYPE, userAction = UserAction.CAPTURE, mockLogger) + val customerPerceivedResponseMonitor = CustomerPerceivedResponseMonitor(userAction = UserAction.CAPTURE, mockLogger) customerPerceivedResponseMonitor.logResult() Assertions.assertThat(mockLogger.errorLogs.count()).isEqualTo(1) Assertions.assertThat(mockLogger.infoLogs.count()).isEqualTo(0) @@ -123,9 +123,9 @@ class ResponseMonitorTest { @Test fun `Validate the attributes of a successful customer perceived response time log`() { val mockLogger = MockLogger() - val vaultType = VaultType.VGS_VAULT_TYPE + val vaultType = VaultType.FORAGE_VAULT_TYPE val userAction = UserAction.CAPTURE - val roundTripResponseMonitor = CustomerPerceivedResponseMonitor(vault = vaultType, userAction = userAction, mockLogger) + val roundTripResponseMonitor = CustomerPerceivedResponseMonitor(userAction = userAction, mockLogger) roundTripResponseMonitor.setEventOutcome(EventOutcome.SUCCESS).setHttpStatusCode(200).logResult() Assertions.assertThat(mockLogger.errorLogs.count()).isEqualTo(0) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/ecom/services/vault/bt/BTResponseParserTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/ecom/services/vault/bt/BTResponseParserTest.kt deleted file mode 100644 index 6ee2059d..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/ecom/services/vault/bt/BTResponseParserTest.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.bt - -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.UnknownErrorApiResponse -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Test - -val dummyRawJson = """{"test":"value"}""" -val dummySuccessResponseJson = Result.success(dummyRawJson) -val dummyFailureProxyErrorJson = Result.failure(Exception("HTTP response body: {\"proxy_error\": \"something went wrong\"}\n")) -val dummyForageErrorJson = Result.failure( - Exception( - """ - HTTP response body: {"error": {"forage_code": "ebt_error_14", "message": "Invalid card number - Re-enter Transaction"}} - HTTP response code: 400 - """.trimIndent() + "\n" // extra newline is intentional!! - ) -) -val dummyEmptyError = Result.failure(Exception("")) -val dummyParsedForageError = ForageApiResponse.Failure( - 400, - "ebt_error_14", - "Invalid card number - Re-enter Transaction" -) - -class BTResponseParserTest { - - @Test - fun `test isNullResponse is always false`() { - val parser = BTResponseParser(dummySuccessResponseJson) - assertThat(parser.isNullResponse).isFalse() - } - - @Test - fun `vaultErrorMsg is empty str when no error`() { - val parser = BTResponseParser(dummySuccessResponseJson) - assertThat(parser.vaultErrorMsg).isNull() - } - - @Test - fun `vaultErrorMsg extract error when it's an error`() { - val exception = Exception("Something went wrong") - val result = Result.failure(exception) - val parser = BTResponseParser(result) - assertThat(exception.message).isEqualTo(parser.vaultErrorMsg) - } - - @Test - fun `rawResponse is string of the Result`() { - val parser = BTResponseParser(dummySuccessResponseJson) - - // so the result will look something like - // "Success({"test": "value"})" since it's a Result instance - assertThat(parser.rawResponse).isEqualTo(dummySuccessResponseJson.toString()) - } - - @Test - fun `vaultError is null when response is successful`() { - val parser = BTResponseParser(dummySuccessResponseJson) - assertThat(parser.vaultError).isNull() - } - - @Test - fun `vaultError is UnknownErrorApiResponse for bodyText with proxy_error`() { - val parser = BTResponseParser(dummyFailureProxyErrorJson) - assertThat(parser.vaultError).isEqualTo(UnknownErrorApiResponse) - } - - @Test - fun `vaultError is null for bodyText with no proxy_error`() { - val parser = BTResponseParser(dummyForageErrorJson) - assertThat(parser.vaultError).isNull() - } - - @Test - fun `forageError is null when body_text=null and statusCode=null`() { - val parser = BTResponseParser(dummyEmptyError) - assertThat(parser.forageError).isNull() - } - - @Test - fun `forageError is successfully parsed from body_text`() { - val parser = BTResponseParser(dummyForageErrorJson) - assertThat(parser.forageError).isEqualTo(dummyParsedForageError) - } - - @Test - fun `response is null for unsuccessful response`() { - val parser = BTResponseParser(dummyEmptyError) - assertThat(parser.successfulResponse).isNull() - } - - @Test - fun `successfully parses Map sync response`() { - val response = Result.success(mapOf("test" to "value")) - val parser = BTResponseParser(response) - assertThat(parser.successfulResponse).isEqualTo( - ForageApiResponse.Success(dummyRawJson) - ) - } - - @Test - fun `successfully parses String JSON sync response`() { - val response = Result.success(dummyRawJson) - val parser = BTResponseParser(response) - assertThat(parser.successfulResponse).isEqualTo( - ForageApiResponse.Success(dummyRawJson) - ) - } - - @Test - fun `successfully parses null empty response from deferred capture`() { - val response = Result.success(null) - val parser = BTResponseParser(response) - assertThat(parser.successfulResponse).isEqualTo( - ForageApiResponse.Success("{}") - ) - } - - @Test - fun `successfully parses empty response from deferred capture`() { - val response = Result.success("") - val parser = BTResponseParser(response) - assertThat(parser.successfulResponse).isEqualTo( - ForageApiResponse.Success("{}") - ) - } - - @Test - fun `throw UnknownBTResponseException when for unexpected response type`() { - val response = Result.success(42) - assertThatThrownBy { BTResponseParser(response) } - .isInstanceOf(UnknownBTSuccessResponse::class.java) - .hasMessageContaining("42") - } -} diff --git a/forage-android/src/test/java/com/joinforage/forage/android/ecom/services/vault/bt/BtFailureResponseRegExpTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/ecom/services/vault/bt/BtFailureResponseRegExpTest.kt deleted file mode 100644 index bfd3e32d..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/ecom/services/vault/bt/BtFailureResponseRegExpTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.joinforage.forage.android.ecom.services.vault.bt - -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test - -class BtFailureResponseRegExpTest { - - @Test - fun `bodyText contains match for valid message`() { - val message = "Exception message... HTTP response body: This is the body\n ..." - val result = Result.failure(Exception(message)) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.bodyText).isEqualTo("This is the body") - } - - @Test - fun `statusCode contains int match for valid message`() { - val message = "Exception message... HTTP response code: 400\n ..." - val result = Result.failure(Exception(message)) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.statusCode).isEqualTo(400) - } - - @Test - fun `bodyText groupValues get(1) does not throw on no match`() { - val message = "Exception message... " - val result = Result.failure(Exception(message)) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.bodyText).isNull() - } - - @Test - fun `statusCode groupValues get(1) does not throw on no match`() { - val message = "Exception message... " - val result = Result.failure(Exception(message)) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.statusCode).isNull() - } - - @Test - fun `bodyText is null for Result Success`() { - val result = Result.success(null) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.bodyText).isNull() - } - - @Test - fun `statusCode is null for Result Success`() { - val result = Result.success(null) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.statusCode).isNull() - } - - @Test - fun `containsProxyError is false for Result Success`() { - val result = Result.success(null) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.containsProxyError).isFalse() - } - - @Test - fun `containsProxyError is false when no proxy_error in error`() { - val message = "Exception message... HTTP response body: {\"blah_error\": \"This is the body\"}\n ..." - val result = Result.failure(Exception(message)) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.containsProxyError).isFalse() - } - - @Test - fun `containsProxyError is true when proxy_error in error`() { - val message = "Exception message... HTTP response body: {\"proxy_error\": \"This is the body\"}\n ..." - val result = Result.failure(Exception(message)) - val regExp = BtFailureResponseRegExp(result) - assertThat(regExp.containsProxyError).isTrue() - } -} diff --git a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/EncryptionKeyFixtures.kt b/forage-android/src/test/java/com/joinforage/forage/android/fixtures/EncryptionKeyFixtures.kt deleted file mode 100644 index 884d878e..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/EncryptionKeyFixtures.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.joinforage.forage.android.fixtures - -import me.jorgecastillo.hiroaki.Method -import me.jorgecastillo.hiroaki.models.PotentialRequestChain -import me.jorgecastillo.hiroaki.models.error -import me.jorgecastillo.hiroaki.models.fileBody -import me.jorgecastillo.hiroaki.models.success -import me.jorgecastillo.hiroaki.whenever -import okhttp3.mockwebserver.MockWebServer - -fun MockWebServer.givenEncryptionKey() = whenever( - method = Method.GET, - sentToPath = "iso_server/encryption_alias" -) - -fun PotentialRequestChain.returnsEncryptionKeySuccessfully() = thenRespond( - success( - jsonBody = fileBody( - "fixtures/encryption/key/successful_get_encryption_key.json" - ) - ) -) - -fun PotentialRequestChain.returnsUnauthorizedEncryptionKey() = thenRespond( - error( - jsonBody = fileBody( - "fixtures/encryption/key/unauthorized_get_encryption_key.json" - ) - ) -) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/GetMessageFixtures.kt b/forage-android/src/test/java/com/joinforage/forage/android/fixtures/GetMessageFixtures.kt deleted file mode 100644 index f7f3c48e..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/GetMessageFixtures.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.joinforage.forage.android.fixtures - -import me.jorgecastillo.hiroaki.models.PotentialRequestChain -import me.jorgecastillo.hiroaki.models.error -import me.jorgecastillo.hiroaki.models.fileBody - -fun PotentialRequestChain.returnsUnauthorized() = thenRespond( - error( - 401, - jsonBody = fileBody( - "fixtures/message/unauthorized_get_message.json" - ) - ) -) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/VaultProxyFixtures.kt b/forage-android/src/test/java/com/joinforage/forage/android/fixtures/VaultProxyFixtures.kt index 1346f6e3..ce312c3e 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/fixtures/VaultProxyFixtures.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/fixtures/VaultProxyFixtures.kt @@ -28,3 +28,10 @@ fun PotentialRequestChain.returnsMalformedError() = thenRespond( jsonBody = inlineBody("{}") ) ) + +fun PotentialRequestChain.returnsUnauthorized() = thenRespond( + error( + 401, + jsonBody = fileBody("fixtures/vault/unauthorized_capture_request.json") + ) +) diff --git a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt deleted file mode 100644 index 2bba9111..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServerUtils.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.joinforage.forage.android.mock - -import org.json.JSONArray -import org.json.JSONObject - -internal fun getVaultMessageResponse(contentId: String): String { - return JSONObject().apply { - put("content_id", contentId) - put("message_type", "0200") - put("status", "sent_to_proxy") - put("failed", false) - put("errors", JSONArray(emptyList())) - }.toString() -} diff --git a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt index 200d4e7d..0cca5e15 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/mock/MockServiceFactory.kt @@ -1,7 +1,5 @@ package com.joinforage.forage.android.mock -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeyService -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys import com.joinforage.forage.android.core.services.forageapi.network.OkHttpClientBuilder import com.joinforage.forage.android.core.services.forageapi.payment.PaymentService import com.joinforage.forage.android.core.services.forageapi.paymentmethod.Balance @@ -41,7 +39,6 @@ internal class MockServiceFactory( // PIN-related interactions const val paymentRef: String = "6ae6a45ff1" const val paymentMethodRef: String = "1f148fe399" - const val contentId: String = "45639248-03f2-498d-8aa8-9ebd1c60ee65" val balance: Balance = EbtBalance( snap = "100.00", cash = "100.00" @@ -53,13 +50,12 @@ internal class MockServiceFactory( balance = null, card = EbtCard( last4 = "7845", - token = "tok_sandbox_sYiPe9Q249qQ5wQyUPP5f7,basis-theory-token", + token = "tok_sandbox_sYiPe9Q249qQ5wQyUPP5f7,basis-theory-token,forage-token", fingerprint = "fingerprint" ), customerId = "test-android-customer-id", reusable = true ) - val mockEncryptionKeys = EncryptionKeys("vgs-alias", "bt-alias") } companion object { @@ -73,7 +69,6 @@ internal class MockServiceFactory( } private val okHttpClient by lazy { createMockHttpClient(logger) } - private val encryptionKeyService by lazy { createEncryptionKeyService() } private val paymentMethodService by lazy { createPaymentMethodService() } private val paymentService by lazy { createPaymentService() } @@ -88,7 +83,6 @@ internal class MockServiceFactory( override fun createCheckBalanceRepository(foragePinEditText: ForagePINEditText): CheckBalanceRepository { return CheckBalanceRepository( vaultSubmitter = mockVaultSubmitter, - encryptionKeyService = encryptionKeyService, paymentMethodService = paymentMethodService, logger = logger ) @@ -97,7 +91,6 @@ internal class MockServiceFactory( override fun createCapturePaymentRepository(foragePinEditText: ForagePINEditText): CapturePaymentRepository { return CapturePaymentRepository( vaultSubmitter = mockVaultSubmitter, - encryptionKeyService = encryptionKeyService, paymentService = paymentService, paymentMethodService = paymentMethodService, logger = logger @@ -107,13 +100,11 @@ internal class MockServiceFactory( override fun createDeferPaymentCaptureRepository(foragePinEditText: ForagePINEditText): DeferPaymentCaptureRepository { return DeferPaymentCaptureRepository( vaultSubmitter = mockVaultSubmitter, - encryptionKeyService = encryptionKeyService, paymentService = paymentService, paymentMethodService = paymentMethodService ) } - private fun createEncryptionKeyService() = EncryptionKeyService(emptyUrl(), okHttpClient, logger) private fun createPaymentMethodService() = PaymentMethodService(emptyUrl(), okHttpClient, logger) private fun createPaymentService() = PaymentService(emptyUrl(), okHttpClient, logger) } diff --git a/forage-android/src/test/java/com/joinforage/forage/android/network/EncryptionKeyServiceTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/network/EncryptionKeyServiceTest.kt deleted file mode 100644 index 66284ab7..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/network/EncryptionKeyServiceTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.joinforage.forage.android.network - -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeyService -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.forageapi.network.OkHttpClientBuilder -import com.joinforage.forage.android.core.services.telemetry.Log -import com.joinforage.forage.android.fixtures.givenEncryptionKey -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully -import com.joinforage.forage.android.fixtures.returnsUnauthorizedEncryptionKey -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import me.jorgecastillo.hiroaki.Method -import me.jorgecastillo.hiroaki.headers -import me.jorgecastillo.hiroaki.internal.MockServerSuite -import me.jorgecastillo.hiroaki.matchers.times -import me.jorgecastillo.hiroaki.verify -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class EncryptionKeyServiceTest : MockServerSuite() { - private val bearerToken: String = "AbCaccesstokenXyz" - private lateinit var encryptionKeyService: EncryptionKeyService - - @Before - override fun setup() { - super.setup() - - encryptionKeyService = EncryptionKeyService( - okHttpClient = OkHttpClientBuilder.provideOkHttpClient(bearerToken), - httpUrl = server.url("").toUrl().toString(), - logger = Log.getSilentInstance() - ) - } - - @Test - fun `it should send the correct headers to get the encryption key`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - - encryptionKeyService.getEncryptionKey() - - server.verify("iso_server/encryption_alias").called( - times = times(1), - method = Method.GET, - headers = headers( - "Authorization" to "Bearer $bearerToken" - ) - ) - } - - @Test - fun `it should return a success when the correct headers to get the encryption key are provided`() = - runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() - - val response = encryptionKeyService.getEncryptionKey() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Success::class.java) - val successResponse = response as ForageApiResponse.Success - - val encryptionKey = EncryptionKeys.ModelMapper.from(successResponse.data) - assertThat(encryptionKey).isEqualTo(EncryptionKeys(vgsAlias = "tok_sandbox_eZeWfkq1AkqYdiAJC8iweE", btAlias = "fake-bt-alias")) - } - - @Test - fun `it should return a failure when auth headers are not provided`() = runTest { - server.givenEncryptionKey().returnsUnauthorizedEncryptionKey() - - val response = encryptionKeyService.getEncryptionKey() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val clientError = response as ForageApiResponse.Failure - val expectedDetail = "Authentication credentials were not provided." - - assertThat(clientError.error.message).isEqualTo(expectedDetail) - } -} diff --git a/forage-android/src/test/java/com/joinforage/forage/android/network/data/CapturePaymentRepositoryTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/network/data/CapturePaymentRepositoryTest.kt index afc9eaa3..86e65435 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/network/data/CapturePaymentRepositoryTest.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/network/data/CapturePaymentRepositoryTest.kt @@ -4,20 +4,16 @@ import com.joinforage.forage.android.core.services.forageapi.network.ForageApiRe import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.CapturePaymentRepository import com.joinforage.forage.android.ecom.ui.element.ForagePINEditText -import com.joinforage.forage.android.fixtures.givenEncryptionKey import com.joinforage.forage.android.fixtures.givenPaymentMethodRef import com.joinforage.forage.android.fixtures.givenPaymentRef -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully import com.joinforage.forage.android.fixtures.returnsFailedPayment import com.joinforage.forage.android.fixtures.returnsFailedPaymentMethod import com.joinforage.forage.android.fixtures.returnsPayment import com.joinforage.forage.android.fixtures.returnsPaymentMethod -import com.joinforage.forage.android.fixtures.returnsUnauthorizedEncryptionKey import com.joinforage.forage.android.mock.MockServiceFactory import com.joinforage.forage.android.mock.MockVaultSubmitter import kotlinx.coroutines.test.runTest import me.jorgecastillo.hiroaki.internal.MockServerSuite -import me.jorgecastillo.hiroaki.matchers.times import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -57,23 +53,10 @@ class CapturePaymentRepositoryTest : MockServerSuite() { } @Test - fun `it should return a failure when the getting the encryption key fails`() = runTest { - server.givenEncryptionKey().returnsUnauthorizedEncryptionKey() - - val response = executeCapturePayment() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val clientError = response as ForageApiResponse.Failure - - assertThat(clientError.error.message).contains("Authentication credentials were not provided.") - } - - @Test - fun `it should return a failure when VGS returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() + fun `it should return a failure when Forage returns a failure`() = runTest { server.givenPaymentRef().returnsPayment() server.givenPaymentMethodRef().returnsPaymentMethod() - val failureResponse = ForageApiResponse.Failure(500, "unknown_server_error", "Some error message from VGS") + val failureResponse = ForageApiResponse.Failure(500, "unknown_server_error", "Some error message from Rosetta") setMockVaultResponse(failureResponse) val response = executeCapturePayment() @@ -83,7 +66,6 @@ class CapturePaymentRepositoryTest : MockServerSuite() { @Test fun `it should return a failure when the get payment returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() server.givenPaymentRef().returnsFailedPayment() val response = executeCapturePayment() @@ -100,7 +82,6 @@ class CapturePaymentRepositoryTest : MockServerSuite() { @Test fun `it should return a failure when the get payment method returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() server.givenPaymentRef().returnsPayment() server.givenPaymentMethodRef().returnsFailedPaymentMethod() @@ -116,8 +97,4 @@ class CapturePaymentRepositoryTest : MockServerSuite() { assertThat(failureResponse.error.code).isEqualTo(expectedForageCode) assertThat(failureResponse.error.httpStatusCode).isEqualTo(expectedStatusCode) } - - companion object { - private const val MAX_POLL_MESSAGE_ATTEMPTS = 10 - } } diff --git a/forage-android/src/test/java/com/joinforage/forage/android/network/data/CheckBalanceRepositoryTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/network/data/CheckBalanceRepositoryTest.kt index 57b45f1e..12373db5 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/network/data/CheckBalanceRepositoryTest.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/network/data/CheckBalanceRepositoryTest.kt @@ -4,11 +4,8 @@ import com.joinforage.forage.android.core.services.forageapi.network.ForageApiRe import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.CheckBalanceRepository import com.joinforage.forage.android.ecom.ui.element.ForagePINEditText -import com.joinforage.forage.android.fixtures.givenEncryptionKey import com.joinforage.forage.android.fixtures.givenPaymentMethodRef -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully import com.joinforage.forage.android.fixtures.returnsPaymentMethod -import com.joinforage.forage.android.fixtures.returnsUnauthorizedEncryptionKey import com.joinforage.forage.android.mock.MockServiceFactory import com.joinforage.forage.android.mock.MockVaultSubmitter import kotlinx.coroutines.test.runTest @@ -37,23 +34,10 @@ class CheckBalanceRepositoryTest : MockServerSuite() { } @Test - fun `it should return a failure when the getting the encryption key fails`() = runTest { - server.givenEncryptionKey().returnsUnauthorizedEncryptionKey() - - val response = executeCheckBalance() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val clientError = response as ForageApiResponse.Failure - - assertThat(clientError.error.message).contains("Authentication credentials were not provided.") - } - - @Test - fun `it should return a failure when VGS returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() + fun `it should return a failure when Forage returns a failure`() = runTest { server.givenPaymentMethodRef().returnsPaymentMethod() - val failureResponse = ForageApiResponse.Failure(500, "unknown_server_error", "Some error message from VGS") + val failureResponse = ForageApiResponse.Failure(500, "unknown_server_error", "Some error message from Rosetta") setMockVaultResponse(failureResponse) val response = executeCheckBalance() @@ -75,8 +59,4 @@ class CheckBalanceRepositoryTest : MockServerSuite() { response = response ) } - - companion object { - private const val MAX_POLL_MESSAGE_ATTEMPTS = 10 - } } diff --git a/forage-android/src/test/java/com/joinforage/forage/android/network/data/DeferPaymentCaptureRepositoryTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/network/data/DeferPaymentCaptureRepositoryTest.kt index b98fee9a..f7338eae 100644 --- a/forage-android/src/test/java/com/joinforage/forage/android/network/data/DeferPaymentCaptureRepositoryTest.kt +++ b/forage-android/src/test/java/com/joinforage/forage/android/network/data/DeferPaymentCaptureRepositoryTest.kt @@ -4,15 +4,12 @@ import com.joinforage.forage.android.core.services.forageapi.network.ForageApiRe import com.joinforage.forage.android.core.services.telemetry.Log import com.joinforage.forage.android.core.services.vault.DeferPaymentCaptureRepository import com.joinforage.forage.android.ecom.ui.element.ForagePINEditText -import com.joinforage.forage.android.fixtures.givenEncryptionKey import com.joinforage.forage.android.fixtures.givenPaymentMethodRef import com.joinforage.forage.android.fixtures.givenPaymentRef -import com.joinforage.forage.android.fixtures.returnsEncryptionKeySuccessfully import com.joinforage.forage.android.fixtures.returnsFailedPayment import com.joinforage.forage.android.fixtures.returnsFailedPaymentMethod import com.joinforage.forage.android.fixtures.returnsPayment import com.joinforage.forage.android.fixtures.returnsPaymentMethod -import com.joinforage.forage.android.fixtures.returnsUnauthorizedEncryptionKey import com.joinforage.forage.android.mock.MockServiceFactory import com.joinforage.forage.android.mock.MockVaultSubmitter import kotlinx.coroutines.test.runTest @@ -41,20 +38,7 @@ class DeferPaymentCaptureRepositoryTest : MockServerSuite() { } @Test - fun `it should return a failure when the getting the encryption key fails`() = runTest { - server.givenEncryptionKey().returnsUnauthorizedEncryptionKey() - - val response = executeDeferPaymentCapture() - - assertThat(response).isExactlyInstanceOf(ForageApiResponse.Failure::class.java) - val clientError = response as ForageApiResponse.Failure - - assertThat(clientError.error.message).contains("Authentication credentials were not provided.") - } - - @Test - fun `it should return a failure when VGS returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() + fun `it should return a failure when Rosetta returns a failure`() = runTest { server.givenPaymentRef().returnsPayment() server.givenPaymentMethodRef().returnsPaymentMethod() @@ -68,7 +52,6 @@ class DeferPaymentCaptureRepositoryTest : MockServerSuite() { @Test fun `it should return a failure when the get payment returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() server.givenPaymentRef().returnsFailedPayment() val response = executeDeferPaymentCapture() @@ -85,7 +68,6 @@ class DeferPaymentCaptureRepositoryTest : MockServerSuite() { @Test fun `it should return a failure when the get payment method returns a failure`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() server.givenPaymentRef().returnsPayment() server.givenPaymentMethodRef().returnsFailedPaymentMethod() @@ -104,7 +86,6 @@ class DeferPaymentCaptureRepositoryTest : MockServerSuite() { @Test fun `it should fail on Vault proxy PIN submission`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() server.givenPaymentRef().returnsPayment() server.givenPaymentRef().returnsPayment() server.givenPaymentMethodRef().returnsPaymentMethod() @@ -126,7 +107,6 @@ class DeferPaymentCaptureRepositoryTest : MockServerSuite() { @Test fun `it should succeed with empty string response`() = runTest { - server.givenEncryptionKey().returnsEncryptionKeySuccessfully() server.givenPaymentRef().returnsPayment() server.givenPaymentRef().returnsPayment() server.givenPaymentMethodRef().returnsPaymentMethod() diff --git a/forage-android/src/test/java/com/joinforage/forage/android/network/data/LaunchDarklyTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/network/data/LaunchDarklyTest.kt deleted file mode 100644 index 7938f92f..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/network/data/LaunchDarklyTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.joinforage.forage.android.network.data - -import android.app.Application -import androidx.test.platform.app.InstrumentationRegistry -import com.joinforage.forage.android.core.services.VaultType -import com.joinforage.forage.android.ecom.services.launchdarkly.ALWAYS_ROSETTA_PERCENT -import com.joinforage.forage.android.ecom.services.launchdarkly.ALWAYS_THIRD_PARTY_PERCENT -import com.joinforage.forage.android.ecom.services.launchdarkly.LDFlags -import com.joinforage.forage.android.ecom.services.launchdarkly.LDManager -import com.joinforage.forage.android.ecom.services.launchdarkly.computeVaultType -import com.launchdarkly.sdk.LDValue -import com.launchdarkly.sdk.android.LDConfig -import com.launchdarkly.sdk.android.integrations.TestData -import kotlinx.coroutines.test.runTest -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -val LD_MOBILE_KEY = "some key" - -@RunWith(RobolectricTestRunner::class) -class LaunchDarklyTest() { - - // NOTE: It is essential that the TestData source live in a - // companion object. For some reason moving this variable - // to be a instance variable instead of a static variable causes - // race conditions within tests - companion object { - private var td: TestData = TestData.dataSource() - private val ldConfig = LDConfig.Builder() - .mobileKey(LD_MOBILE_KEY) - .dataSource(td) - .build() - } - - @Test - fun `Test computeVaultType returns Forage Vault if percent is 100f`() { - assertThat(computeVaultType(ALWAYS_ROSETTA_PERCENT)).isEqualTo(VaultType.FORAGE_VAULT_TYPE) - } - - @Test - fun `Test computeVaultType returns BT percent is 0f`() { - assertThat(computeVaultType(ALWAYS_THIRD_PARTY_PERCENT)).isEqualTo(VaultType.BT_VAULT_TYPE) - } - - @Test - fun `It should default to using Rosetta and honor flag updates`() = runTest { - // set up LDManager, importantly, we're not giving it any value for rosettaTrafficPercentage or - // primaryTrafficPercent since we want to test it defaults to Rosetta - val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application - LDManager.initialize(app, ldConfig) - - // it should default to using Rosetta as the vault provider when no flags are set - val original = LDManager.getVaultProvider() - assertThat(original).isEqualTo(VaultType.FORAGE_VAULT_TYPE) - - // Set the test data to send all traffic to rosetta - td.update( - td.flag(LDFlags.ROSETTA_TRAFFIC_PERCENTAGE).variations( - LDValue.of( - ALWAYS_ROSETTA_PERCENT - ) - ) - ) - - val postRosettaUpdate = LDManager.getVaultProvider() - assertThat(postRosettaUpdate).isEqualTo(VaultType.FORAGE_VAULT_TYPE) - - // Set the test data to send all traffic to third-party - td.update( - td.flag(LDFlags.ROSETTA_TRAFFIC_PERCENTAGE).variations( - LDValue.of( - ALWAYS_THIRD_PARTY_PERCENT - ) - ) - ) - - // it should use BT - val post3PUpdate = LDManager.getVaultProvider() - assertThat(post3PUpdate).isEqualTo(VaultType.BT_VAULT_TYPE) - } - - @Test - fun `Default polling intervals`() = runTest { - // set up LDManager - val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application - LDManager.initialize(app, ldConfig) - - // Set the test data to be {"intervals" : [1000]} - td.update(td.flag(LDFlags.ISO_POLLING_WAIT_INTERVALS).variations(LDValue.buildObject().put("intervals", LDValue.Convert.Long.arrayFrom(List(1) { 1000L })).build())) - - val pollingIntervals = LDManager.getPollingIntervals() - assertThat(pollingIntervals).isEqualTo(longArrayOf(1000L)) - } -} diff --git a/forage-android/src/test/java/com/joinforage/forage/android/utils/UtilsTest.kt b/forage-android/src/test/java/com/joinforage/forage/android/utils/UtilsTest.kt deleted file mode 100644 index 8cdc6712..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/utils/UtilsTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.joinforage.forage.android.utils - -import com.joinforage.forage.android.core.services.getJitterAmount -import kotlinx.coroutines.test.runTest -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import kotlin.random.Random - -class UtilsTest { - @Test - fun `Jitter value should be in range -25 to 25`() = runTest { - val fixedRandomZero = Random(13) - val jitterZero = getJitterAmount(fixedRandomZero) - assertThat(jitterZero).isEqualTo(0) - - val fixedRandomMin = Random(16) - val jitterMin = getJitterAmount(fixedRandomMin) - assertThat(jitterMin).isEqualTo(-25) - - val fixedRandomMax = Random(60) - val jitterMax = getJitterAmount(fixedRandomMax) - assertThat(jitterMax).isEqualTo(25) - } -} 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 5d514021..08d53e4f 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,7 +1,6 @@ package com.joinforage.forage.android.vault 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 import com.joinforage.forage.android.core.services.forageapi.network.UnknownErrorApiResponse import com.joinforage.forage.android.core.services.forageapi.paymentmethod.PaymentMethod @@ -30,10 +29,8 @@ class AbstractVaultSubmitterTest : MockServerSuite() { } companion object { - private val mockEncryptionKeys = EncryptionKeys("vgs-alias", "bt-alias") private val mockPaymentMethod = MockServiceFactory.ExpectedData.mockPaymentMethod private val mockVaultParams = VaultSubmitterParams( - encryptionKeys = mockEncryptionKeys, idempotencyKey = "mock-idempotency-key", merchantId = "1234567", path = "/api/payments/abcdefg123/capture/", @@ -179,29 +176,18 @@ class AbstractVaultSubmitterTest : MockServerSuite() { @Test fun `grabs the correct vault token`() = runTest { - val basisTheorySubmitter = object : ConcreteVaultSubmitter( + val forageSubmitter = object : ConcreteVaultSubmitter( collector = mockCollector, logger = mockLogger ) { override fun getVaultToken(paymentMethod: PaymentMethod): String? { - return pickVaultTokenByIndex(paymentMethod, 1) + return pickVaultTokenByIndex(paymentMethod, 2) } } - val vgsSubmitter = object : ConcreteVaultSubmitter( - collector = mockCollector, - logger = mockLogger - ) { - override fun getVaultToken(paymentMethod: PaymentMethod): String? { - return pickVaultTokenByIndex(paymentMethod, 0) - } - } + val forageVaultToken = forageSubmitter.getVaultToken(mockVaultParams.paymentMethod) - val basisTheoryVaultToken = basisTheorySubmitter.getVaultToken(mockVaultParams.paymentMethod) - val vgsVaultToken = vgsSubmitter.getVaultToken(mockVaultParams.paymentMethod) - - assertEquals("tok_sandbox_sYiPe9Q249qQ5wQyUPP5f7", vgsVaultToken) - assertEquals("basis-theory-token", basisTheoryVaultToken) + assertEquals("forage-token", forageVaultToken) } @Test @@ -220,11 +206,11 @@ class AbstractVaultSubmitterTest : MockServerSuite() { concreteVaultSubmitter.submit(mockVaultParams) assertThat(mockLogger.infoLogs).anyMatch { logEntry -> - logEntry.getMessage().contains("[Metrics] Received response from vgs proxy") + logEntry.getMessage().contains("[Metrics] Received response from forage proxy") } val attributes = mockLogger.getMetricsLog().getAttributes() assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("vgs") + assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") assertThat(attributes.getValue("action").toString()).isEqualTo("capture") assertThat(attributes.getValue("event_name").toString()).isEqualTo("vault_response") assertThat(attributes.getValue("log_type").toString()).isEqualTo("metric") @@ -247,7 +233,6 @@ class AbstractVaultSubmitterTest : MockServerSuite() { concreteVaultSubmitter.submit( VaultSubmitterParams( - encryptionKeys = mockEncryptionKeys, idempotencyKey = "mock-idempotency-key", merchantId = "1234567", path = "/api/payment_methods/abcdefg123/balance/", @@ -261,11 +246,11 @@ class AbstractVaultSubmitterTest : MockServerSuite() { val metricsLog = mockLogger.getMetricsLog() assertThat(mockLogger.infoLogs).anyMatch { logEntry -> - logEntry.getMessage().contains("[Metrics] Received response from vgs proxy") + logEntry.getMessage().contains("[Metrics] Received response from forage proxy") } val attributes = metricsLog.getAttributes() assertThat(attributes.getValue("response_time_ms").toString().toDouble()).isGreaterThan(0.0) - assertThat(attributes.getValue("vault_type").toString()).isEqualTo("vgs") + assertThat(attributes.getValue("vault_type").toString()).isEqualTo("forage") assertThat(attributes.getValue("action").toString()).isEqualTo("balance") assertThat(attributes.getValue("event_name").toString()).isEqualTo("vault_response") assertThat(attributes.getValue("path").toString()).isEqualTo("/api/payment_methods/abcdefg123/balance/") @@ -281,10 +266,7 @@ internal open class ConcreteVaultSubmitter( collector = collector, logger = logger ) { - override val vaultType: VaultType = VaultType.VGS_VAULT_TYPE - override fun parseEncryptionKey(encryptionKeys: EncryptionKeys): String { - return "mock-encryption-key-alias" - } + override val vaultType: VaultType = VaultType.FORAGE_VAULT_TYPE override suspend fun submitProxyRequest( vaultProxyRequest: VaultProxyRequest 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 4efd7485..00000000 --- a/forage-android/src/test/java/com/joinforage/forage/android/vault/BasisTheoryPinSubmitterTest.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.joinforage.forage.android.vault - -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.EnvConfig -import com.joinforage.forage.android.core.services.ForageConstants -import com.joinforage.forage.android.core.services.forageapi.encryptkey.EncryptionKeys -import com.joinforage.forage.android.core.services.forageapi.network.ForageApiResponse -import com.joinforage.forage.android.core.services.vault.SecurePinCollector -import com.joinforage.forage.android.core.services.vault.VaultProxyRequest -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.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 submitter: BasisTheoryPinSubmitter - private lateinit var mockBasisTheory: BasisTheoryElements - private lateinit var mockBasisTheoryTextElement: TextElement - private lateinit var mockApiProxy: ProxyApi - - private val mockCollector = object : SecurePinCollector { - override fun clearText() {} - override fun isComplete() = true - } - - @Before - fun setUp() { - super.setup() - - mockLogger = MockLogger() - - // Use Mockito judiciously (mainly for mocking views)! - // Opt for dependency injection and inheritance over Mockito - mockBasisTheory = mock(BasisTheoryElements::class.java) - mockBasisTheoryTextElement = mock(TextElement::class.java) - - // ensure we don't make any live requests! - mockBasisTheoryResponse(Result.success(mapOf("mySuccessfulKey" to "mySuccessfulValue"))) - - submitter = BasisTheoryPinSubmitter( - btTextElement = mockBasisTheoryTextElement, - collector = mockCollector, - envConfig = EnvConfig.Sandbox, - 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 `grabs the right encryption key`() = runTest { - val encryptionKeys = EncryptionKeys("vgs_123", "bt_456") - val res = submitter.parseEncryptionKey(encryptionKeys) - assertEquals("bt_456", res) - } - - @Test - fun `Basis Theory request receives expected params`() = runTest { - 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") - .setHeader(ForageConstants.Headers.API_VERSION, "2024-01-08") - .setToken("45320ce0-1a3c-4c64-970c-51ed7db34548") - .setPath("/api/payment_methods/defghij123/balance/") - .setHeader(ForageConstants.Headers.BT_PROXY_KEY, EnvConfig.Sandbox.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", - "API-VERSION" to "2024-01-08", - "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 `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 error = (result as ForageApiResponse.Failure).error - assertEquals("[basis_theory] Received error from basis_theory: $basisTheoryErrorMessage", mockLogger.errorLogs.last().getMessage()) - assertEquals("Unknown Server Error", error.message) - assertEquals(500, error.httpStatusCode) - assertEquals("unknown_server_error", error.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 error = (result as ForageApiResponse.Failure).error - 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.", error.message) - assertEquals(400, error.httpStatusCode) - assertEquals("too_many_requests", error.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 error = (result as ForageApiResponse.Failure).error - assertEquals( - "[basis_theory] Received malformed response from basis_theory: Failure(java.lang.RuntimeException: Malformed error!)", - mockLogger.errorLogs.last().getMessage() - ) - assertEquals("Unknown Server Error", error.message) - assertEquals(500, error.httpStatusCode) - assertEquals("unknown_server_error", error.code) - } -} 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 3abc706b..054ad973 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 @@ -225,7 +225,6 @@ class RosettaPinSubmitterTest() : MockServerSuite() { * But the x-key header is not applicable to Rosetta client * and is omitted via [RosettaPinSubmitter.parseEncryptionKey] */ - .setHeader(ForageConstants.Headers.X_KEY, "22320ce0-1a3c-4c64-970c-51ed7db34548") .setHeader(ForageConstants.Headers.MERCHANT_ACCOUNT, mockData.merchantId) .setHeader(ForageConstants.Headers.IDEMPOTENCY_KEY, MOCK_IDEMPOTENCY_KEY) .setHeader(ForageConstants.Headers.TRACE_ID, MOCK_TRACE_ID) @@ -236,7 +235,6 @@ class RosettaPinSubmitterTest() : MockServerSuite() { } private fun buildMockVaultParams(path: String) = VaultSubmitterParams( - encryptionKeys = MockServiceFactory.ExpectedData.mockEncryptionKeys, idempotencyKey = MOCK_IDEMPOTENCY_KEY, merchantId = "1234567", path = "/api/payments/abcdefg123/capture/", diff --git a/forage-android/src/test/resources/fixtures/encryption/key/successful_get_encryption_key.json b/forage-android/src/test/resources/fixtures/encryption/key/successful_get_encryption_key.json deleted file mode 100644 index 9e2d6bc0..00000000 --- a/forage-android/src/test/resources/fixtures/encryption/key/successful_get_encryption_key.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "alias": "tok_sandbox_eZeWfkq1AkqYdiAJC8iweE", - "bt_alias": "fake-bt-alias" -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/encryption/key/unauthorized_get_encryption_key.json b/forage-android/src/test/resources/fixtures/encryption/key/unauthorized_get_encryption_key.json deleted file mode 100644 index 96742e4f..00000000 --- a/forage-android/src/test/resources/fixtures/encryption/key/unauthorized_get_encryption_key.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "path": "iso_server/encryption_alias", - "errors": [{ - "code": "unauthorized", - "message": "Authentication credentials were not provided.", - "source": { - "resource": "Access Token", - "ref": "" - } - }] -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/message/expired_card_message_response.json b/forage-android/src/test/resources/fixtures/message/expired_card_message_response.json deleted file mode 100644 index 6864c729..00000000 --- a/forage-android/src/test/resources/fixtures/message/expired_card_message_response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "content_id": "36058ff7-0e9d-4025-94cd-80ef04a3bb1c", - "message_type": "0200", - "status": "received_on_django", - "failed": true, - "errors": [ - { - "status_code": 400, - "forage_code": "ebt_error_54", - "message": "Expired card - Expired Card" - } - ] -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/message/failed_get_message.json b/forage-android/src/test/resources/fixtures/message/failed_get_message.json deleted file mode 100644 index 31de7303..00000000 --- a/forage-android/src/test/resources/fixtures/message/failed_get_message.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "content_id": "c2bf2b14-415f-4cb3-8687-7eea06f75369", - "message_type": "0200", - "status": "received_on_django", - "failed": true, - "errors": [ - { - "status_code": 504, - "forage_code": "ebt_error_91", - "message": "Authorizer not available (time-out) - Host Not Available" - } - ] -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/message/send_to_proxy_message.json b/forage-android/src/test/resources/fixtures/message/send_to_proxy_message.json deleted file mode 100644 index 3c58726d..00000000 --- a/forage-android/src/test/resources/fixtures/message/send_to_proxy_message.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "content_id": "45639248-03f2-498d-8aa8-9ebd1c60ee65", - "message_type": "0200", - "status": "sent_to_proxy", - "failed": false, - "errors": [] -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/message/successful_completed_message.json b/forage-android/src/test/resources/fixtures/message/successful_completed_message.json deleted file mode 100644 index c3686aa4..00000000 --- a/forage-android/src/test/resources/fixtures/message/successful_completed_message.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "content_id": "d789c086-9c4f-41c3-854a-1c436eee1d63", - "message_type": "0200", - "status": "completed", - "failed": false, - "errors": [] -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/payments/capture/successful_send_to_proxy_response.json b/forage-android/src/test/resources/fixtures/payments/capture/successful_send_to_proxy_response.json deleted file mode 100644 index 019e6cbe..00000000 --- a/forage-android/src/test/resources/fixtures/payments/capture/successful_send_to_proxy_response.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "messages": [ - { - "content_id": "44302be3-8fc6-4df6-9123-0ba4417d53f8", - "message_type": "0200", - "status": "sent_to_proxy", - "failed": false, - "errors": [] - } - ] -} \ No newline at end of file diff --git a/forage-android/src/test/resources/fixtures/message/unauthorized_get_message.json b/forage-android/src/test/resources/fixtures/vault/unauthorized_capture_request.json similarity index 78% rename from forage-android/src/test/resources/fixtures/message/unauthorized_get_message.json rename to forage-android/src/test/resources/fixtures/vault/unauthorized_capture_request.json index 67237403..c9f549a4 100644 --- a/forage-android/src/test/resources/fixtures/message/unauthorized_get_message.json +++ b/forage-android/src/test/resources/fixtures/vault/unauthorized_capture_request.json @@ -1,5 +1,5 @@ { - "path": "/api/message/1c53e9e0-92e9-4568-a128-deecf4c2194a/", + "path": "/api/payments/abc123ref/capture", "errors": [ { "code": "missing_merchant_account",