@file:OptIn(ExperimentalCryptoOnramp::class) package com.reactnativestripesdk import com.stripe.android.core.StripeError import com.stripe.android.core.exception.APIException import com.stripe.android.crypto.onramp.ExperimentalCryptoOnramp import com.stripe.android.crypto.onramp.exception.APIErrorContext import com.stripe.android.crypto.onramp.exception.SDKVersion import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class OnrampErrorsTest { @Test fun createOnrampFailedError_withAppAttestationException_preservesApiFields() { val error = createApiOnrampException( className = "com.stripe.android.crypto.onramp.exception.AppAttestationException", reason = "android_package_name_mismatch", operation = "configure", appPackageName = "com.example.app", mode = "test", sdkVersions = listOf( SDKVersion(name = "stripe-android", version = "23.9.1"), SDKVersion(name = "stripe-react-native", version = "0.66.0"), ), userMessage = "App attestation failed.", apiErrorCode = "link_failed_to_attest_request", apiErrorType = "api_error", apiErrorMessage = "Attestation request could not be verified.", apiUserMessage = "App attestation failed.", docUrl = "https://docs.stripe.com/errors/app-attestation", requestId = "req_attestation", ) val details = createOnrampFailedError(error).getMap("error") assertNotNull(details) assertEquals("Failed", details!!.getString("code")) assertEquals("AppAttestationError", details.getString("onrampErrorType")) assertEquals("App attestation failed.", details.getString("message")) assertEquals("App attestation failed.", details.getString("localizedMessage")) assertEquals("App attestation failed.", details.getString("userMessage")) assertEquals("link_failed_to_attest_request", details.getString("stripeErrorCode")) assertEquals("link_failed_to_attest_request", details.getString("apiErrorCode")) assertEquals("api_error", details.getString("type")) assertEquals("api_error", details.getString("apiErrorType")) assertEquals("android_package_name_mismatch", details.getString("reason")) assertEquals("req_attestation", details.getString("requestId")) assertEquals( "Attestation request could not be verified.", details.getString("apiErrorMessage"), ) assertEquals("App attestation failed.", details.getString("apiUserMessage")) assertEquals( "https://docs.stripe.com/errors/app-attestation", details.getString("docUrl"), ) assertTrue(details.getString("developerMessage")!!.contains("operation: configure")) assertTrue(details.getString("developerMessage")!!.contains("request_id: req_attestation")) } @Test fun createOnrampFailedError_withUncategorizedApiError_preservesApiFields() { val error = createApiOnrampException( className = "com.stripe.android.crypto.onramp.exception.UncategorizedException", reason = "consumer_session_expired", operation = "authorize", appPackageName = "com.example.app", mode = "live", sdkVersions = listOf(SDKVersion(name = "stripe-android", version = "23.9.1")), userMessage = "Session expired. Please sign in again.", apiErrorCode = "consumer_session_expired", apiErrorType = "authentication_error", apiErrorMessage = "The consumer session has expired.", apiUserMessage = "Session expired. Please sign in again.", docUrl = "https://docs.stripe.com/error-codes/consumer-session-expired", requestId = "req_auth", ) val details = createOnrampFailedError(error).getMap("error") assertNotNull(details) assertEquals("UncategorizedApiError", details!!.getString("onrampErrorType")) assertEquals("consumer_session_expired", details.getString("stripeErrorCode")) assertEquals("authentication_error", details.getString("type")) assertEquals("Session expired. Please sign in again.", details.getString("message")) assertEquals("Session expired. Please sign in again.", details.getString("userMessage")) assertEquals("consumer_session_expired", details.getString("reason")) assertEquals("req_auth", details.getString("requestId")) assertTrue(details.getString("developerMessage")!!.contains("Code: consumer_session_expired")) } @Test fun createOnrampFailedError_withWalletOwnershipApiError_mapsSpecificErrorType() { val testCases = mapOf( "crypto_onramp_invalid_wallet_ownership_signature" to "InvalidWalletOwnershipSignatureError", "crypto_onramp_wallet_ownership_challenge_expired" to "WalletOwnershipChallengeExpiredError", "crypto_onramp_invalid_wallet_ownership_challenge" to "InvalidWalletOwnershipChallengeError", "crypto_onramp_wallet_not_found" to "WalletNotFoundError", "crypto_onramp_unsupported_network" to "UnsupportedNetworkError", ) testCases.forEach { (apiErrorCode, expectedType) -> val error = createApiOnrampException( className = "com.stripe.android.crypto.onramp.exception.UncategorizedException", reason = "wallet_verification_failed", operation = "submitWalletOwnershipSignature", appPackageName = "com.example.app", mode = "test", sdkVersions = listOf(SDKVersion(name = "stripe-android", version = "23.15.0")), userMessage = "Wallet ownership verification failed.", apiErrorCode = apiErrorCode, apiErrorType = "invalid_request_error", apiErrorMessage = "Wallet ownership verification failed.", apiUserMessage = "Please try again.", requestId = "req_wallet_verification", ) val details = createOnrampFailedError(error).getMap("error") assertNotNull(apiErrorCode, details) assertEquals(apiErrorCode, expectedType, details!!.getString("onrampErrorType")) assertEquals(apiErrorCode, apiErrorCode, details.getString("apiErrorCode")) assertEquals(apiErrorCode, "wallet_verification_failed", details.getString("reason")) assertEquals(apiErrorCode, "req_wallet_verification", details.getString("requestId")) } } @Test fun createOnrampFailedError_withAppAttestationUnavailableException_preservesSdkErrorFields() { val underlyingError = IllegalStateException("Native Link is not available") val error = createAppAttestationUnavailableException( underlyingError = underlyingError, operation = "configure", appPackageName = "com.example.app", mode = "test", sdkVersions = listOf( SDKVersion(name = "stripe-android", version = "23.10.0"), SDKVersion(name = "stripe-react-native", version = "0.67.0"), ), userMessage = "This app couldn't be verified. Contact the app developer for help.", ) val details = createOnrampFailedError(error).getMap("error") assertNotNull(details) assertEquals("Failed", details!!.getString("code")) assertEquals("AppAttestationUnavailableError", details.getString("onrampErrorType")) assertEquals( "This app couldn't be verified. Contact the app developer for help.", details.getString("message"), ) assertEquals( "This app couldn't be verified. Contact the app developer for help.", details.getString("localizedMessage"), ) assertEquals( "This app couldn't be verified. Contact the app developer for help.", details.getString("userMessage"), ) assertEquals("app_attestation_unavailable", details.getString("stripeErrorCode")) assertTrue(details.getString("developerMessage")!!.contains("operation: configure")) } private fun createApiOnrampException( className: String, reason: String, operation: String, appPackageName: String, mode: String, sdkVersions: List, userMessage: String, apiErrorCode: String, apiErrorType: String, apiErrorMessage: String, apiUserMessage: String, docUrl: String? = null, requestId: String, ): Exception { val stripeError = StripeError( type = apiErrorType, message = apiErrorMessage, code = apiErrorCode, docUrl = docUrl, ) val cause = APIException(stripeError = stripeError, requestId = requestId) val apiErrorContext = APIErrorContext( reason = reason, apiErrorCode = apiErrorCode, apiErrorType = apiErrorType, apiErrorMessage = apiErrorMessage, apiUserMessage = apiUserMessage, docUrl = docUrl, underlyingError = cause, ) val diagnosticContext = createDiagnosticContext(sdkVersions, operation, appPackageName, mode) val constructor = Class .forName(className) .getDeclaredConstructor( APIErrorContext::class.java, diagnosticContext.javaClass, String::class.java, ).apply { isAccessible = true } return constructor.newInstance( apiErrorContext, diagnosticContext, userMessage, ) as Exception } private fun createAppAttestationUnavailableException( underlyingError: Throwable, operation: String, appPackageName: String, mode: String, sdkVersions: List, userMessage: String, ): Exception { val diagnosticContext = createDiagnosticContext(sdkVersions, operation, appPackageName, mode) val exceptionClass = Class.forName( "com.stripe.android.crypto.onramp.exception.AppAttestationUnavailableException", ) val constructor = exceptionClass .getDeclaredConstructor( Throwable::class.java, diagnosticContext.javaClass, String::class.java, ) .apply { isAccessible = true } return constructor.newInstance( underlyingError, diagnosticContext, userMessage, ) as Exception } private fun createDiagnosticContext( sdkVersions: List, operation: String, appPackageName: String, mode: String, ): Any { val diagnosticContextClass = Class.forName("com.stripe.android.crypto.onramp.exception.DiagnosticContext") val constructor = diagnosticContextClass .getDeclaredConstructor( List::class.java, String::class.java, String::class.java, String::class.java, ).apply { isAccessible = true } return constructor.newInstance(sdkVersions, operation, appPackageName, mode) } }