diff --git a/forgerock-integration-tests/build.gradle.kts b/forgerock-integration-tests/build.gradle.kts index a6e47f4d..3750da0d 100644 --- a/forgerock-integration-tests/build.gradle.kts +++ b/forgerock-integration-tests/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { androidTestImplementation(libs.androidx.biometric.ktx) androidTestImplementation(libs.nimbus.jose.jwt) androidTestImplementation(libs.okhttp) + androidTestImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.kotlinx.coroutines.test) //For Application Pin androidTestImplementation(libs.bcpkix.jdk15on) @@ -50,4 +52,8 @@ dependencies { //App Integrity androidTestImplementation(libs.integrity) + + // Captcha + androidTestImplementation(libs.play.services.safetynet) + androidTestImplementation(libs.recaptchaEnterprise) } \ No newline at end of file diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackBaseTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackBaseTest.kt new file mode 100644 index 00000000..9d4da833 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackBaseTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions +import org.forgerock.android.auth.FRAuth +import org.forgerock.android.auth.FROptionsBuilder +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.Logger.Companion.set +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListener +import org.json.JSONObject +import org.junit.After +import org.junit.Assert +import org.junit.BeforeClass +import org.junit.Test +import java.util.concurrent.ExecutionException + +/** + * e2e tests for [ReCaptchaEnterpriseCallback] + */ +open class ReCaptchaEnterpriseCallbackBaseTest { + @After + fun logoutSession() { + if (FRSession.getCurrentSession() != null) { + FRSession.getCurrentSession().logout() + } + } + + companion object { + val context: Context = ApplicationProvider.getApplicationContext() + val application: Application = ApplicationProvider.getApplicationContext() + + protected const val AM_URL: String = "https://localam.petrov.ca/openam" + protected const val REALM: String = "root" + protected const val OAUTH_CLIENT: String = "AndroidTest" + protected const val OAUTH_REDIRECT_URI: String = "org.forgerock.demo:/oauth2redirect" + protected const val SCOPE: String = "openid profile email address phone" + const val TREE: String = "TEST-e2e-recaptcha-enterprise" + const val USERNAME: String = "sdkuser" + const val RECAPTCHA_SITE_KEY: String = "6LfAykUqAAAAAE6aZOg9pNiS3XduyGZ5y-8U-z8B" + + @JvmStatic + @BeforeClass + fun setUpSDK(): Unit { + set(Logger.Level.DEBUG) + + val options = FROptionsBuilder.build { + server { + url = AM_URL + realm = REALM + } + oauth { + oauthClientId = OAUTH_CLIENT + oauthRedirectUri = OAUTH_REDIRECT_URI + oauthCacheSeconds = 0 + oauthScope = SCOPE + } + service { + authServiceName = TREE + } + } + + FRAuth.start(context, options) + } + } +} + + + + diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackFailureTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackFailureTest.kt new file mode 100644 index 00000000..5dd547ee --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackFailureTest.kt @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import com.google.android.recaptcha.RecaptchaException +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListener +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Failure e2e tests for the [ReCaptchaEnterpriseCallback] + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ReCaptchaEnterpriseCallbackFailureTest : ReCaptchaEnterpriseCallbackBaseTest() { + + @Test + fun test01RecaptchaEnterpriseScoreFailure() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "score_failure" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + + // Execute recaptcha - this should pass... + runBlocking { + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.FAILURE + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + Assertions.assertThat(message).contains("VALIDATION_ERROR") + + node.next(context, nodeListener) + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test02RecaptchaEnterpriseInvalidProjectId() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "invalid_project_id" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + runBlocking { + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.FAILURE + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + Assertions.assertThat(message).contains("API_ERROR") + Assertions.assertThat(message).contains("reCAPTCHA Enterprise API has not been used in project invalid before or it is disabled") + + node.next(context, nodeListener) + + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test03RecaptchaEnterpriseInvalidVerificationUrl() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "invalid_verification_url" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + runBlocking { + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.FAILURE + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + Assertions.assertThat(message).contains("INVALID_SECRET_KEY:Secret key could not be retrieved") + + node.next(context, nodeListener) + + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test04RecaptchaEnterpriseInvalidSecretKey() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "invalid_secret_key" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + runBlocking { + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.FAILURE + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + Assertions.assertThat(message).contains("INVALID_SECRET_KEY:Secret key could not be retrieved") + + node.next(context, nodeListener) + + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test05RecaptchaEnterpriseCustomClientError() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "custom_client_error" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + runBlocking { + callback.setClientError("CUSTOM_CLIENT_ERROR") + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.FAILURE + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + Assertions.assertThat(message).contains("CUSTOM_CLIENT_ERROR") + + node.next(context, nodeListener) + + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test06RecaptchaEnterpriseInvalidSiteKey() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + val recaptchaExceptionHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "invalid_site_key" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + + try { + runBlocking { + callback.execute(application = application) + } + } + catch (e: Exception) { + recaptchaExceptionHit[0]++ + if(e is RecaptchaException) { + Logger.error("RecaptchaException", "${e.errorCode}:${e.message}") + } + Logger.error("RecaptchaException", e.message) + } + node.next(context, nodeListener) + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.FAILURE + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + Assertions.assertThat(message).contains("CLIENT_ERROR") + + node.next(context, nodeListener) + + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertEquals(1, recaptchaExceptionHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } +} + + + + diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackSuccessTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackSuccessTest.kt new file mode 100644 index 00000000..73074aaf --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaEnterpriseCallbackSuccessTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListener +import org.json.JSONObject +import org.junit.Assert +import org.junit.Test + + +/** + * Success e2e tests for the [ReCaptchaEnterpriseCallback] + */ +class ReCaptchaEnterpriseCallbackSuccessTest : ReCaptchaEnterpriseCallbackBaseTest() { + + @Test + fun testRecaptchaEnterpriseSuccess() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "success" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + + // Make sure that the ReCaptcha site key is set correctly + Assert.assertNotNull(callback.reCaptchaSiteKey) + Assertions.assertThat(callback.reCaptchaSiteKey).isEqualTo(RECAPTCHA_SITE_KEY) + + // Execute recaptcha - this should pass... + runBlocking { + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + // Make sure that the journey finishes with success + nodeTextOutputHit[0]++ + + // Note: The journey sends back to us the content of CaptchaEnterpriseNode.ASSESSMENT_RESULT + // in the message of the TextOutputCallback... so we can parse it here and verify results... + val message = node.getCallback(TextOutputCallback::class.java).message + + val jsonObject = JSONObject(message); + + Assert.assertNotNull(jsonObject) + Logger.debug("RecaptchaEnterpriseCallbackTest", jsonObject.toString(2)) + + val name = jsonObject.getString("name") + val token = jsonObject.getJSONObject("event").getString("token") + val siteKey = jsonObject.getJSONObject("event").getString("siteKey") + val userAgent = jsonObject.getJSONObject("event").getString("userAgent") + val userIpAddress = jsonObject.getJSONObject("event").getString("userIpAddress") + val score = jsonObject.getJSONObject("riskAnalysis").getDouble("score") + val valid = jsonObject.getJSONObject("tokenProperties").getBoolean("valid") + val action = jsonObject.getJSONObject("tokenProperties").getString("action") + val androidPackageName = jsonObject.getJSONObject("tokenProperties").getString("androidPackageName") + + // Ensure that the name is "recaptcha" + Assertions.assertThat(name).contains("projects/") + + // Ensure that the token is not empty + Assertions.assertThat(token).isNotEmpty() + + // Ensure that the valid is true + Assertions.assertThat(valid).isTrue() + + // Ensure that the score is between 0 and 1 + Assertions.assertThat(score).isBetween(0.0, 1.0) + + // Ensure that the action is "login" (this is the default action) + Assertions.assertThat(action).isEqualTo("login") + + // Ensure that the siteKey is the expected one + Assertions.assertThat(siteKey).isEqualTo(RECAPTCHA_SITE_KEY) + + // Ensure that the userAgent is not empty + Assertions.assertThat(userAgent).contains("okhttp") + + // Ensure that the userIPAddress is not empty + Assertions.assertThat(userIpAddress).isNotEmpty() + + // Ensure that the androidPackageName is the expected one + Assertions.assertThat(androidPackageName).isEqualTo("org.forgerock.android.integration.test") + + node.next(context, nodeListener) + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun testRecaptchaEnterpriseCustomAction() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "custom_action" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + + // Execute recaptcha - this should pass... + runBlocking { + callback.execute(application = application, action = "custom_action") + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + // Make sure that the journey finishes with success + nodeTextOutputHit[0]++ + + val message = node.getCallback(TextOutputCallback::class.java).message + val jsonObject = JSONObject(message); + Logger.debug("RecaptchaEnterpriseCallbackTest", jsonObject.toString(2)) + + val action = jsonObject.getJSONObject("tokenProperties").getString("action") + + // Ensure that the action is "custom_action" + Assertions.assertThat(action).isEqualTo("custom_action") + + node.next(context, nodeListener) + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun testRecaptchaEnterpriseCustomPayload() = runTest { + val nodeCaptchaHit = intArrayOf(0) + val nodeTextOutputHit = intArrayOf(0) + + val nodeListenerFuture = object : RecaptchaEnterpriseNodeListener( + context, "custom_payload" + ) { + val nodeListener: NodeListener = this + + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ReCaptchaEnterpriseCallback::class.java) != null) { + val callback = node.getCallback(ReCaptchaEnterpriseCallback::class.java) + nodeCaptchaHit[0]++ + + val customPayloadString = """ + { + "firewallPolicyEvaluation": false, + "express": false, + "transaction_data": { + "transaction_id": "custom-payload-1234567890", + "payment_method": "credit-card", + "card_bin": "1111", + "card_last_four": "1234", + "currency_code": "CAD", + "value": 12.34, + "user": { + "email": "sdkuser@example.com" + }, + "billing_address": { + "recipient": "Sdk User", + "address": [ + "3333 Random Road" + ], + "locality": "Courtenay", + "administrative_area": "BC", + "region_code": "CA", + "postal_code": "V2V 2V2" + } + } + } + """.trimIndent() + val customPayload = JSONObject(customPayloadString) + // Execute recaptcha - this should pass... + runBlocking { + callback.setPayload(customPayload) + callback.execute(application = application) + node.next(context, nodeListener) + } + } + if (node.getCallback(TextOutputCallback::class.java) != null) { + // Make sure that the journey finishes with success + nodeTextOutputHit[0]++ + + val message = node.getCallback(TextOutputCallback::class.java).message + val jsonObject = JSONObject(message); + Logger.debug("RecaptchaEnterpriseCallbackTest", jsonObject.toString(2)) + + val transactionId = jsonObject.getJSONObject("event").getJSONObject("transactionData").getString("transactionId") + val paymentMethod = jsonObject.getJSONObject("event").getJSONObject("transactionData").getString("paymentMethod") + val cardBin = jsonObject.getJSONObject("event").getJSONObject("transactionData").getString("cardBin") + val cardLastFour = jsonObject.getJSONObject("event").getJSONObject("transactionData").getString("cardLastFour") + + // Ensure that the custom payload has been sent to the recaptcha assessment API + Assertions.assertThat(transactionId).isEqualTo("custom-payload-1234567890") + Assertions.assertThat(paymentMethod).isEqualTo("credit-card") + Assertions.assertThat(cardBin).isEqualTo("1111") + Assertions.assertThat(cardLastFour).isEqualTo("1234") + + // This one is set in AM via the CaptchaEnterpriseNode.PAYLOAD shared state variable + val accountId = jsonObject.getJSONObject("event").getJSONObject("userInfo").getString("accountId") + Assertions.assertThat(accountId).isEqualTo("user_account_id_123") + + node.next(context, nodeListener) + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, TREE, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertEquals(1, nodeCaptchaHit[0].toLong()) + Assert.assertEquals(1, nodeTextOutputHit[0].toLong()) + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } +} + + + + diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/RecaptchaEnterpriseNodeListener.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/RecaptchaEnterpriseNodeListener.kt new file mode 100644 index 00000000..7c7b31e4 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/RecaptchaEnterpriseNodeListener.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import android.content.Context +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListenerFuture + +open class RecaptchaEnterpriseNodeListener( + private val context: Context, + private val nodeConfiguration: String +) : NodeListenerFuture() { + override fun onCallbackReceived(node: Node) { + if (node.getCallback(ChoiceCallback::class.java) != null) { + val choiceCallback = node.getCallback( + ChoiceCallback::class.java + ) + val choices = choiceCallback.choices + val choiceIndex = choices.indexOf(nodeConfiguration) + choiceCallback.setSelectedIndex(choiceIndex) + node.next(context, this) + } + if (node.getCallback(NameCallback::class.java) != null) { + node.getCallback(NameCallback::class.java) + .setName(ReCaptchaEnterpriseCallbackBaseTest.USERNAME) + node.next(context, this) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec37c540..c534b462 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ play-services-location = "21.3.0" play-services-safetynet = "18.1.0" powermock-module-junit4 = "2.0.9" powermock-api-mockito2 = "2.0.9" -recaptchaEnterprise = "18.6.0" +recaptchaEnterprise = "18.6.1" rules = "1.6.1" security-crypto = "1.1.0-alpha06" firebase-crashlytics-buildtools = "3.0.2"