diff --git a/libs/SalesforceSDK/res/values/sf__strings.xml b/libs/SalesforceSDK/res/values/sf__strings.xml index 47906df90e..61377c18ae 100644 --- a/libs/SalesforceSDK/res/values/sf__strings.xml +++ b/libs/SalesforceSDK/res/values/sf__strings.xml @@ -124,6 +124,7 @@ Toggle Web Server Toggle Hybrid Token Toggle Force Advanced Authentication + Toggle Ephemeral Advanced Authentication Session Toggle DPoP Toggle Dynamic Config Consumer Key Field diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt index 5bfec6cba7..e16ca85d6a 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt @@ -534,6 +534,15 @@ open class SalesforceSDKManager protected constructor( @set:Synchronized var customTabBrowser: String? = "com.android.chrome" + /** + * Whether advanced authentication requests an ephemeral Custom Tab. A browser that does not + * support ephemeral browsing may use a regular Custom Tab instead. Defaults to true. + */ + @get:JvmName("shouldUseEphemeralSessionForAdvancedAuth") + @set:Synchronized + @Volatile + var useEphemeralSessionForAdvancedAuth = true + // Backing field for [useWebServerAuthentication]. The SDK reads this directly so its own // internal use of the flag doesn't trigger the deprecation warning on the public property. @Volatile @@ -1980,6 +1989,7 @@ open class SalesforceSDKManager protected constructor( "Use Web Server Authentication" to "$_useWebServerAuthentication", "Use Hybrid Authentication Token" to "$useHybridAuthentication", "Force Advanced Authentication" to "$_forceAdvancedAuthentication", + "Use Ephemeral Session for Advanced Authentication" to "$useEphemeralSessionForAdvancedAuth", "My Domain Browser Login Enabled" to "$isBrowserLoginEnabled", "IDP Enabled" to "$isIDPLoginFlowEnabled", "Identity Provider" to "$isIdentityProvider", diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt index a6f026eef9..7f695b2917 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt @@ -37,7 +37,6 @@ import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP import android.app.AlertDialog import android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE -import android.content.pm.PackageManager import android.content.pm.PackageManager.FEATURE_FACE import android.content.pm.PackageManager.FEATURE_IRIS import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY @@ -97,6 +96,7 @@ import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.biometric.BiometricPrompt.AuthenticationResult import androidx.biometric.BiometricPrompt.PromptInfo import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.OPEN_IN_BROWSER_STATE_OFF import androidx.browser.customtabs.ExperimentalInitialNavigationCanLeaveBrowser @@ -1139,10 +1139,18 @@ open class LoginActivity : FragmentActivity() { @OptIn(ExperimentalInitialNavigationCanLeaveBrowser::class) @VisibleForTesting - internal fun loadLoginPageInCustomTab(loginUrl: String, customTabLauncher: ActivityResultLauncher) { + internal fun loadLoginPageInCustomTab( + loginUrl: String, + customTabLauncher: ActivityResultLauncher, + callbackSchemeRegistered: (String) -> Boolean = ::isCallbackSchemeRegistered, + ) { completedViaBrowserTab = true registerAuthTypeFeatureGlobal() - SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_BROWSER_LOGIN) + val sdkManager = SalesforceSDKManager.getInstance() + sdkManager.registerUsedAppFeature(FEATURE_BROWSER_LOGIN) + + val customTabBrowser = sdkManager.customTabBrowser + val customTabBrowserExists = doesBrowserExist(customTabBrowser) val customTabsIntent = CustomTabsIntent.Builder().apply { /* * Set a custom animation to slide in and out for Chrome custom tab @@ -1169,6 +1177,10 @@ open class LoginActivity : FragmentActivity() { setOpenInBrowserButtonState(OPEN_IN_BROWSER_STATE_OFF) setInstantAppsEnabled(false) setBackgroundInteractionEnabled(false) + configureEphemeralBrowsing( + enabled = sdkManager.useEphemeralSessionForAdvancedAuth, + requestedBrowser = customTabBrowser.takeIf { customTabBrowserExists }, + ) }.build() /* @@ -1178,14 +1190,13 @@ open class LoginActivity : FragmentActivity() { * - If getCustomTabBrowser() returns null * - Or if the specified browser is not installed */ - val customTabBrowser = SalesforceSDKManager.getInstance().customTabBrowser - if (doesBrowserExist(customTabBrowser)) { + if (customTabBrowserExists) { customTabsIntent.intent.setPackage(customTabBrowser) } val urlString = buildCustomTabAuthorizeUrl(loginUrl, completedViaAdminCustomTab) - if (!isCallbackSchemeRegistered(viewModel.oAuthConfig.redirectUri)) { + if (!callbackSchemeRegistered(viewModel.oAuthConfig.redirectUri)) { val scheme = viewModel.oAuthConfig.redirectUri.toUri().scheme ?: viewModel.oAuthConfig.redirectUri e(TAG, "Advanced auth misconfiguration: redirect URI scheme '$scheme' has no " + @@ -1213,6 +1224,25 @@ open class LoginActivity : FragmentActivity() { } } + @VisibleForTesting + internal fun CustomTabsIntent.Builder.configureEphemeralBrowsing( + enabled: Boolean, + requestedBrowser: String?, + isSupported: (Context, String) -> Boolean = CustomTabsClient::isEphemeralBrowsingSupported, + ) { + if (!enabled) return + + val provider = requestedBrowser ?: CustomTabsClient.getPackageName(this@LoginActivity, null) + if (provider != null && !isSupported(this@LoginActivity, provider)) { + w( + TAG, + "Ephemeral browsing was requested, but Custom Tabs provider '$provider' does not " + + "advertise support. The browser may use a regular session.", + ) + } + setEphemeralBrowsingEnabled(true) + } + /** * Appends `sdkInfo` and `auth_trigger` to the authorize URL for the native browser (Custom * Tab) path only — the WebView path's real `User-Agent` header already carries this diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt index 48d0093e26..e81408e102 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt @@ -103,6 +103,9 @@ class LoginOptionsActivity: ComponentActivity() { // suppress the deprecation nudge here (it fires on the public property from outside the SDK). @Suppress("DEPRECATION") val forceAdvancedAuth = MutableLiveData(SalesforceSDKManager.getInstance().forceAdvancedAuthentication) + val useEphemeralSessionForAdvancedAuth = MutableLiveData( + SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth, + ) val useDPoP = MutableLiveData(SalesforceSDKManager.getInstance().useDPoP) @OptIn(ExperimentalMaterial3Api::class) @@ -133,6 +136,12 @@ class LoginOptionsActivity: ComponentActivity() { value -> SalesforceSDKManager.getInstance().forceAdvancedAuthentication = value }, ) + useEphemeralSessionForAdvancedAuth.observe( + /* owner = */ this, + Observer { + value -> SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = value + }, + ) useDPoP.observe( /* owner = */ this, Observer { @@ -158,6 +167,7 @@ class LoginOptionsActivity: ComponentActivity() { useWebServer, useHybridToken, forceAdvancedAuth, + useEphemeralSessionForAdvancedAuth, useDPoP, SalesforceSDKManager.getInstance().debugOverrideAppConfig, ) @@ -337,6 +347,7 @@ fun LoginOptionsScreen( useWebServer: MutableLiveData, useHybridToken: MutableLiveData, forceAdvancedAuth: MutableLiveData, + useEphemeralSessionForAdvancedAuth: MutableLiveData, useDPoP: MutableLiveData, overrideConfig: OAuthConfig?, bootConfig: BootConfig = BootConfig.getBootConfig(LocalContext.current), @@ -365,6 +376,11 @@ fun LoginOptionsScreen( stringResource(R.string.sf__login_options_force_advanced_auth_toggle_content_description), forceAdvancedAuth, ) + OptionToggle( + "Use Ephemeral Session", + stringResource(R.string.sf__login_options_ephemeral_session_toggle_content_description), + useEphemeralSessionForAdvancedAuth, + ) OptionToggle( "Use DPoP", stringResource(R.string.sf__login_options_dpop_toggle_content_description), @@ -587,6 +603,7 @@ fun LoginOptionsScreenPreview() { useWebServer = MutableLiveData(true), useHybridToken = MutableLiveData(false), forceAdvancedAuth = MutableLiveData(true), + useEphemeralSessionForAdvancedAuth = MutableLiveData(true), useDPoP = MutableLiveData(false), overrideConfig = null, bootConfig = object : BootConfig() { @@ -595,4 +612,4 @@ fun LoginOptionsScreenPreview() { }, sdkManager = null, ) -} \ No newline at end of file +} diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java index 71ec4fb79e..46dcdc411c 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java @@ -125,6 +125,26 @@ public void testDefaultTheme() { SalesforceSDKTestManager.getInstance().isDarkTheme()); } + /** + * Test the default and Java API for ephemeral advanced-auth sessions. + */ + @Test + public void testUseEphemeralSessionForAdvancedAuthDefaultsToTrueAndCanBeUpdated() { + SalesforceSDKTestManager.resetInstance(); + SalesforceSDKTestManager.init(getInstrumentation().getTargetContext(), MainActivity.class); + final SalesforceSDKManager sdkManager = SalesforceSDKTestManager.getInstance(); + + try { + Assert.assertTrue(sdkManager.shouldUseEphemeralSessionForAdvancedAuth()); + + sdkManager.setUseEphemeralSessionForAdvancedAuth(false); + + Assert.assertFalse(sdkManager.shouldUseEphemeralSessionForAdvancedAuth()); + } finally { + SalesforceSDKTestManager.resetInstance(); + } + } + /** * Test setting dark theme. */ diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt index cd5ba54f43..0fdc78dd48 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt @@ -28,6 +28,11 @@ package com.salesforce.androidsdk.ui import android.content.Intent import android.webkit.WebView +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.app.ActivityOptionsCompat import androidx.core.net.toUri import androidx.lifecycle.Lifecycle.State.RESUMED import androidx.lifecycle.Lifecycle.State.STARTED @@ -38,6 +43,7 @@ import com.salesforce.androidsdk.app.Features import com.salesforce.androidsdk.app.SalesforceSDKManager import com.salesforce.androidsdk.config.LoginServerManager.PRODUCTION_LOGIN_URL import com.salesforce.androidsdk.config.LoginServerManager.WELCOME_LOGIN_URL +import com.salesforce.androidsdk.config.OAuthConfig import com.salesforce.androidsdk.security.BiometricAuthenticationManager import com.salesforce.androidsdk.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HINT import com.salesforce.androidsdk.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HOST @@ -126,6 +132,91 @@ class LoginActivityScenarioTest { } } + @Test + @Suppress("DEPRECATION") + fun loadLoginPageInCustomTab_usesCurrentEphemeralSettingAndConfiguredBrowser() { + val sdkManager = SalesforceSDKManager.getInstance() + val originalForceAdvancedAuth = sdkManager.forceAdvancedAuthentication + val originalCustomTabBrowser = sdkManager.customTabBrowser + val originalUseEphemeralSession = sdkManager.useEphemeralSessionForAdvancedAuth + val affectedFeatures = listOf( + Features.FEATURE_BROWSER_LOGIN, + Features.FEATURE_AUTH_TYPE_WEB_SERVER_NON_HYBRID, + Features.FEATURE_AUTH_TYPE_WEB_SERVER_HYBRID, + Features.FEATURE_AUTH_TYPE_USER_AGENT_NON_HYBRID, + Features.FEATURE_AUTH_TYPE_USER_AGENT_HYBRID, + Features.FEATURE_AUTH_TYPE_NATIVE, + ) + val originalFeatureState = affectedFeatures.associateWith(sdkManager::isGlobalFeatureRegistered) + val configuredBrowser = getApplicationContext().packageName + val launchedIntents = mutableListOf() + val launcher = object : ActivityResultLauncher() { + override fun launch(input: Intent, options: ActivityOptionsCompat?) { + launchedIntents += input + } + + override fun unregister() = Unit + + override val contract: ActivityResultContract = StartActivityForResult() + } + + try { + sdkManager.forceAdvancedAuthentication = false + sdkManager.customTabBrowser = configuredBrowser + + launch( + Intent(getApplicationContext(), LoginActivity::class.java) + ).use { activityScenario -> + activityScenario.onActivity { activity -> + activity.viewModel.oAuthConfig = OAuthConfig( + consumerKey = "test-consumer-key", + redirectUri = "testsfdc://success/done", + ) + sdkManager.useEphemeralSessionForAdvancedAuth = true + activity.loadLoginPageInCustomTab( + "https://example.com/services/oauth2/authorize?client_id=abc", + launcher, + callbackSchemeRegistered = { true }, + ) + + sdkManager.useEphemeralSessionForAdvancedAuth = false + activity.loadLoginPageInCustomTab( + "https://example.com/services/oauth2/authorize?client_id=abc", + launcher, + callbackSchemeRegistered = { true }, + ) + } + } + + assertEquals(2, launchedIntents.size) + assertEquals(configuredBrowser, launchedIntents[0].`package`) + assertTrue( + launchedIntents[0].getBooleanExtra( + CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, + false, + ) + ) + assertEquals(configuredBrowser, launchedIntents[1].`package`) + assertFalse( + launchedIntents[1].getBooleanExtra( + CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, + false, + ) + ) + } finally { + sdkManager.forceAdvancedAuthentication = originalForceAdvancedAuth + sdkManager.customTabBrowser = originalCustomTabBrowser + sdkManager.useEphemeralSessionForAdvancedAuth = originalUseEphemeralSession + originalFeatureState.forEach { (feature, wasRegistered) -> + if (wasRegistered) { + sdkManager.registerUsedAppFeature(feature) + } else { + sdkManager.unregisterUsedAppFeature(feature) + } + } + } + } + @Test fun viewModelFrontDoorBridgeCodeVerifier_UpdatesOn_onCreateWithQrCodeLoginIntent() { val uri = "app://android/login/qr/?bridgeJson=%7B%22pkce_code_verifier%22%3A%22__CODE_VERIFIER__%22%2C%22frontdoor_bridge_url%22%3A%22https%3A%2F%2Fmobilesdk.my.salesforce.com%2Fsecur%2Ffrontdoor.jsp%3Fotp%3D__OTP__%26startURL%3D%252Fservices%252Foauth2%252Fauthorize%253Fresponse_type%253Dcode%2526client_id%253D__CONSUMER_KEY__%2526redirect_uri%253Dtestsfdc%25253A%25252F%25252F%25252Fmobilesdk%25252Fdetect%25252Foauth%25252Fdone%2526code_challenge%253D__CODE_CHALLENGE__%26cshc%3D__CSHC__%22%7D".toUri() diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt index 36c57daf0c..c0a1a5d585 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt @@ -41,6 +41,8 @@ import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED import androidx.biometric.BiometricManager.BIOMETRIC_STATUS_UNKNOWN +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.ui.graphics.Color import androidx.core.net.toUri import androidx.lifecycle.MediatorLiveData @@ -50,6 +52,7 @@ import com.salesforce.androidsdk.app.SalesforceSDKManager import com.salesforce.androidsdk.rest.ClientManager import com.salesforce.androidsdk.rest.RestClient import com.salesforce.androidsdk.rest.RestClient.OAuthRefreshInterceptor +import com.salesforce.androidsdk.util.SalesforceSDKLogger import com.salesforce.androidsdk.ui.LoginActivity.Companion.ABOUT_BLANK import com.salesforce.androidsdk.ui.LoginActivity.Companion.AUTH_TRIGGER_FORCE_ADVANCED_AUTH import com.salesforce.androidsdk.ui.LoginActivity.Companion.AUTH_TRIGGER_LOGIN_FOR_ADMIN @@ -70,6 +73,7 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.unmockkObject import io.mockk.verify @@ -371,6 +375,120 @@ class LoginActivityTest { // endregion + // region Ephemeral Custom Tabs + + @Test + fun configureEphemeralBrowsing_whenEnabledAndSupported_setsIntentExtra() { + val activity = mockk(relaxed = true) + every { + with(activity) { + any().configureEphemeralBrowsing(any(), any(), any()) + } + } answers { callOriginal() } + val builder = CustomTabsIntent.Builder() + + with(activity) { + builder.configureEphemeralBrowsing( + enabled = true, + requestedBrowser = "com.example.browser", + isSupported = { _, _ -> true }, + ) + } + + assertTrue(builder.build().isEphemeralBrowsingEnabled) + } + + @Test + fun configureEphemeralBrowsing_whenDisabled_doesNotSetIntentExtra() { + val activity = mockk(relaxed = true) + every { + with(activity) { + any().configureEphemeralBrowsing(any(), any(), any()) + } + } answers { callOriginal() } + val builder = CustomTabsIntent.Builder() + + with(activity) { + builder.configureEphemeralBrowsing( + enabled = false, + requestedBrowser = "com.example.browser", + isSupported = { _, _ -> error("Capability must not be checked when disabled") }, + ) + } + + assertFalse(builder.build().isEphemeralBrowsingEnabled) + } + + @Test + fun configureEphemeralBrowsing_whenSupportIsNotAdvertised_warnsAndStillSetsIntentExtra() { + mockkStatic(SalesforceSDKLogger::class) + every { SalesforceSDKLogger.w(any(), any()) } just Runs + val activity = mockk(relaxed = true) + every { + with(activity) { + any().configureEphemeralBrowsing(any(), any(), any()) + } + } answers { callOriginal() } + val builder = CustomTabsIntent.Builder() + val provider = "com.example.browser" + + with(activity) { + builder.configureEphemeralBrowsing( + enabled = true, + requestedBrowser = provider, + isSupported = { _, _ -> false }, + ) + } + + assertTrue(builder.build().isEphemeralBrowsingEnabled) + verify(exactly = 1) { + SalesforceSDKLogger.w( + any(), + match { it.contains(provider) && it.contains("does not advertise support") }, + ) + } + } + + @Test + fun configureEphemeralBrowsing_withoutRequestedBrowser_checksDefaultProvider() { + mockkStatic(CustomTabsClient::class) + mockkStatic(SalesforceSDKLogger::class) + every { SalesforceSDKLogger.w(any(), any()) } just Runs + val activity = mockk(relaxed = true) + every { + with(activity) { + any().configureEphemeralBrowsing(any(), any(), any()) + } + } answers { callOriginal() } + val builder = CustomTabsIntent.Builder() + val provider = "com.example.defaultbrowser" + every { CustomTabsClient.getPackageName(activity, null) } returns provider + var checkedProvider: String? = null + + with(activity) { + builder.configureEphemeralBrowsing( + enabled = true, + requestedBrowser = null, + isSupported = { _, packageName -> + checkedProvider = packageName + false + }, + ) + } + + assertEquals(provider, checkedProvider) + assertTrue(builder.build().isEphemeralBrowsingEnabled) + verify(exactly = 1) { CustomTabsClient.getPackageName(activity, null) } + verify(exactly = 1) { + SalesforceSDKLogger.w( + any(), + match { it.contains(provider) && it.contains("does not advertise support") }, + ) + } + } + + // endregion + // region Custom Tab toolbar color @Test @@ -458,21 +576,35 @@ class LoginActivityTest { @Test @Suppress("DEPRECATION") - fun buildCustomTabAuthorizeUrl_withSharedBrowserSession_omitsPromptLogin() { + fun buildCustomTabAuthorizeUrl_promptLoginDependsOnlyOnSharedBrowserSession() { val loginUrl = "https://example.com/services/oauth2/authorize?client_id=abc" val sdkManager = mockk(relaxed = true) every { sdkManager.forceAdvancedAuthentication } returns false every { sdkManager.getUserAgent("") } returns "SalesforceMobileSDK/test" val activity = mockk(relaxed = true) - every { activity.sharedBrowserSession } returns true every { activity.buildCustomTabAuthorizeUrl(any(), any(), any()) } answers { callOriginal() } - val result = activity.buildCustomTabAuthorizeUrl(loginUrl, false, sdkManager) + listOf(false, true).forEach { useEphemeralSession -> + every { + sdkManager.useEphemeralSessionForAdvancedAuth + } returns useEphemeralSession - assertFalse("prompt=login should be omitted for a shared browser session", - result.contains("prompt=login")) + every { activity.sharedBrowserSession } returns false + val isolatedResult = activity.buildCustomTabAuthorizeUrl(loginUrl, false, sdkManager) + assertTrue( + "prompt=login should be appended regardless of the ephemeral setting", + isolatedResult.contains("prompt=login"), + ) + + every { activity.sharedBrowserSession } returns true + val sharedResult = activity.buildCustomTabAuthorizeUrl(loginUrl, false, sdkManager) + assertFalse( + "prompt=login should be omitted regardless of the ephemeral setting", + sharedResult.contains("prompt=login"), + ) + } } // endregion diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt index 0ea375c71c..c77383e8db 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt @@ -78,6 +78,7 @@ class LoginOptionsActivityTest { private var originalUseWebServer: Boolean = false private var originalUseHybridToken: Boolean = false private var originalForceAdvancedAuth: Boolean = true + private var originalUseEphemeralSessionForAdvancedAuth: Boolean = true private lateinit var dynamicToggle: SemanticsNodeInteraction private lateinit var consumerKeyField: SemanticsNodeInteraction private lateinit var redirectUriField: SemanticsNodeInteraction @@ -85,6 +86,7 @@ class LoginOptionsActivityTest { private lateinit var webserverToggle: SemanticsNodeInteraction private lateinit var hybridToggle: SemanticsNodeInteraction private lateinit var forceAdvancedAuthToggle: SemanticsNodeInteraction + private lateinit var ephemeralSessionToggle: SemanticsNodeInteraction private lateinit var saveButton: SemanticsNodeInteraction @Before @@ -94,6 +96,8 @@ class LoginOptionsActivityTest { originalUseWebServer = SalesforceSDKManager.getInstance().useWebServerAuthentication originalUseHybridToken = SalesforceSDKManager.getInstance().useHybridAuthentication originalForceAdvancedAuth = SalesforceSDKManager.getInstance().forceAdvancedAuthentication + originalUseEphemeralSessionForAdvancedAuth = + SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth SalesforceSDKManager.getInstance().loginDevMenuReload = false dynamicToggle = composeTestRule.onNodeWithContentDescription( @@ -117,6 +121,9 @@ class LoginOptionsActivityTest { forceAdvancedAuthToggle = composeTestRule.onNodeWithContentDescription( composeTestRule.activity.getString(R.string.sf__login_options_force_advanced_auth_toggle_content_description), ) + ephemeralSessionToggle = composeTestRule.onNodeWithContentDescription( + composeTestRule.activity.getString(R.string.sf__login_options_ephemeral_session_toggle_content_description), + ) saveButton = composeTestRule.onNodeWithText( composeTestRule.activity.getString(R.string.sf__login_options_save_and_login), ) @@ -129,6 +136,8 @@ class LoginOptionsActivityTest { SalesforceSDKManager.getInstance().useWebServerAuthentication = originalUseWebServer SalesforceSDKManager.getInstance().useHybridAuthentication = originalUseHybridToken SalesforceSDKManager.getInstance().forceAdvancedAuthentication = originalForceAdvancedAuth + SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = + originalUseEphemeralSessionForAdvancedAuth SalesforceSDKManager.getInstance().debugOverrideAppConfig = null SalesforceSDKManager.getInstance().loginDevMenuReload = false } @@ -286,6 +295,46 @@ class LoginOptionsActivityTest { ) } + @Test + fun loginOptionsActivity_EphemeralSessionToggle_ReflectsAndUpdatesSdkManager() { + composeTestRule.activity.runOnUiThread { + SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = false + composeTestRule.activity.useEphemeralSessionForAdvancedAuth.value = false + } + composeTestRule.waitForIdle() + + ephemeralSessionToggle.performScrollTo() + ephemeralSessionToggle.assertIsDisplayed() + ephemeralSessionToggle.assertIsOff() + + ephemeralSessionToggle.performClick() + composeTestRule.waitForIdle() + + ephemeralSessionToggle.assertIsOn() + assertTrue(SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth) + } + + @Test + fun devSupportInfo_IncludesEphemeralAdvancedAuthentication_WithCurrentValue() { + SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = true + val authConfigOn = SalesforceSDKManager.getInstance().devSupportInfo.authConfigSection?.second + assertEquals( + "true", + authConfigOn?.find { + it.first == "Use Ephemeral Session for Advanced Authentication" + }?.second, + ) + + SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = false + val authConfigOff = SalesforceSDKManager.getInstance().devSupportInfo.authConfigSection?.second + assertEquals( + "false", + authConfigOff?.find { + it.first == "Use Ephemeral Session for Advanced Authentication" + }?.second, + ) + } + @Test @Suppress("DEPRECATION") // Deliberately exercises the deprecated forceAdvancedAuthentication flag. fun devSupportInfo_IncludesForceAdvancedAuthentication_WithCurrentValue() { @@ -560,4 +609,4 @@ class LoginOptionsActivityTest { } // endregion -} \ No newline at end of file +}