From 9a5a4a04d3677b6f73e3750b05fd872b6cb801b8 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Thu, 28 May 2026 18:55:18 -0700 Subject: [PATCH 1/3] Do not add My Domain from Welcome Discovery --- .../androidsdk/app/SalesforceSDKManager.kt | 10 ++- .../salesforce/androidsdk/ui/LoginActivity.kt | 20 +++--- .../androidsdk/ui/LoginViewModel.kt | 6 +- .../app/SalesforceSDKManagerTests.kt | 27 ++++++++ .../androidsdk/auth/LoginViewModelTest.kt | 34 ++++++++-- .../androidsdk/ui/LoginActivityTest.kt | 64 ++++++++++++++++++- 6 files changed, 145 insertions(+), 16 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt index 4879c60103..07021dc498 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt @@ -2001,15 +2001,23 @@ open class SalesforceSDKManager protected constructor( * @param httpAccess The HTTP access to use for API integration. Defaults * to null to use the default HTTP access. This parameter is intended for * testing purposes only and should not be used in release builds. + * @param loginServerUrl Optional override for the login server URL whose + * authentication configuration should be fetched. Defaults to null which + * uses the [LoginServerManager]'s currently selected server. Callers + * driving a transient server (e.g., a Welcome Discovery My Domain that is + * intentionally not persisted to the server list) should pass the + * transient URL here so browser login and app attestation are configured + * for the right server. * @param completion An optional function to invoke at the end of the action */ internal fun fetchAuthenticationConfiguration( httpAccess: HttpAccess? = null, + loginServerUrl: String? = null, completion: (() -> Unit), ) = CoroutineScope(Default).launch { // If this takes more than five seconds it can cause Android's application not responding report. withTimeoutOrNull(5000L) { - val loginServer = loginServerManager.selectedLoginServer.url.trim() + val loginServer = (loginServerUrl ?: loginServerManager.selectedLoginServer.url).trim() if (loginServer == PRODUCTION_LOGIN_URL || loginServer == WELCOME_LOGIN_URL || loginServer == SANDBOX_LOGIN_URL || !isHttpsUrl(loginServer) || loginServer.toHttpUrlOrNull() == null) { setBrowserLoginEnabled( browserLoginEnabled = false, diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt index 65a8b0842d..c61feb0167 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt @@ -981,10 +981,10 @@ open class LoginActivity : FragmentActivity() { * mirroring iOS' `simulatedDomainDiscoveryResult` hook. * * The WebView's current URL is set to ABOUT_BLANK before dispatching so that the - * subsequent OAuth-URL reload triggered by [applySalesforceWelcomeLoginHintAndHost]'s - * `addCustomLoginServer` is not suppressed by [LoginViewModel.LoginUrlSource]'s - * same-host short-circuit when the simulated host happens to equal the previously - * selected server. + * subsequent OAuth-URL reload triggered by [applySalesforceWelcomeLoginHintAndHost] + * setting [LoginViewModel.pendingServer] is not suppressed by + * [LoginViewModel.LoginUrlSource]'s same-host short-circuit when the simulated host + * happens to equal the previously selected server. * * @return Boolean true when the simulated result was applied and the welcome.salesforce.com * discovery WebView load should be skipped, false otherwise. @@ -1018,16 +1018,20 @@ open class LoginActivity : FragmentActivity() { * Salesforce Welcome for external linking to default login with a specific * login username hint and My Domain log in server. It is also used in the * Salesforce Welcome Discovery flow. + * + * The My Domain drives [LoginViewModel.pendingServer] for this login + * attempt only and is intentionally not persisted to + * [com.salesforce.androidsdk.config.LoginServerManager]. + * * @param intent The activity's intent */ - private fun applySalesforceWelcomeLoginHintAndHost(intent: Intent) { - val loginServerManager = SalesforceSDKManager.getInstance().loginServerManager - + @VisibleForTesting + internal fun applySalesforceWelcomeLoginHintAndHost(intent: Intent) { viewModel.loginHint = intent.getStringExtra(EXTRA_KEY_LOGIN_HINT) intent.getStringExtra(EXTRA_KEY_LOGIN_HOST)?.let { loginHost -> val loginUrl = "https://$loginHost" - loginServerManager.addCustomLoginServer(loginHost, loginUrl) + viewModel.pendingServer.value = loginUrl } } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginViewModel.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginViewModel.kt index aa2aef18d5..2eddb50fed 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginViewModel.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginViewModel.kt @@ -344,7 +344,7 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() { */ internal fun applyPendingServer( sdkManager: SalesforceSDKManager = SalesforceSDKManager.getInstance(), - pendingLoginServer: String? + pendingLoginServer: String?, ) { val pendingLoginServerUnwrapped: String = pendingLoginServer ?: return @@ -358,7 +358,9 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() { // Fetch the pending login server's authentication configuration to set the selected login server and OAuth authorization URL. else { authenticationConfigurationFetchJob?.cancel() - authenticationConfigurationFetchJob = sdkManager.fetchAuthenticationConfiguration { + authenticationConfigurationFetchJob = sdkManager.fetchAuthenticationConfiguration( + loginServerUrl = pendingLoginServerUnwrapped, + ) { selectedServer.postValue(pendingLoginServerUnwrapped) authenticationConfigurationFetchJob = null } diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt index 169146ec8b..93fa516cfc 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt @@ -288,6 +288,33 @@ class SalesforceSDKManagerTests { assertFalse(SalesforceSDKManager.getInstance().isShareBrowserSessionEnabled) } + @Test + fun fetchAuthenticationConfiguration_withLoginServerUrlOverride_usesOverrideOverPersistedSelectedServer() { + + SalesforceSDKManager.getInstance().loginServerManager.setSelectedLoginServer( + LoginServer("Production", PRODUCTION_LOGIN_URL, false) + ) + + SalesforceSDKManager.getInstance().isBrowserLoginEnabled = true + SalesforceSDKManager.getInstance().isShareBrowserSessionEnabled = true + + runBlocking { + SalesforceSDKManager.getInstance().fetchAuthenticationConfiguration( + httpAccess = httpAccess, + loginServerUrl = "https://acme.my.salesforce.com", + ) { + /* Completion Does Not Require Verification */ + }.join() + } + + assertFalse(SalesforceSDKManager.getInstance().isBrowserLoginEnabled) + assertFalse(SalesforceSDKManager.getInstance().isShareBrowserSessionEnabled) + assertEquals( + PRODUCTION_LOGIN_URL, + SalesforceSDKManager.getInstance().loginServerManager.selectedLoginServer.url, + ) + } + @Test fun salesforceSdkManager_ClearsAppAttestationHostName_ForNonMyDomainServer() { diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt index ea1017343e..e1ee99d456 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt @@ -422,6 +422,26 @@ class LoginViewModelTest { assertTrue(expectedResult.matches(result)) } + @Test + fun applyPendingServer_withWelcomeDiscoveryMyDomain_doesNotPolluteLoginServerManager() { + val loginServerManager = SalesforceSDKManager.getInstance().loginServerManager + val originalSelectedServer = loginServerManager.selectedLoginServer + val originalServersCount = loginServerManager.loginServers.size + val myDomainUrl = "https://acme.my.salesforce.com" + + viewModel.pendingServer.value = myDomainUrl + viewModel.applyPendingServer(pendingLoginServer = myDomainUrl) + Thread.sleep(200) + + assertEquals(myDomainUrl, viewModel.selectedServer.value) + assertNotNull(viewModel.loginUrl.value) + assertTrue(viewModel.loginUrl.value!!.startsWith(myDomainUrl)) + + assertEquals(originalSelectedServer, loginServerManager.selectedLoginServer) + assertEquals(originalServersCount, loginServerManager.loginServers.size) + assertNull(loginServerManager.getLoginServerFromURL(myDomainUrl)) + } + @Test fun testGetValidSeverUrl() { assertNull(viewModel.getValidServerUrl("")) @@ -1283,7 +1303,7 @@ class LoginViewModelTest { viewModel.applyPendingServer(sdkManager = sdkManager, pendingLoginServer = null) assert(viewModel.previousPendingServer == null) - verify(exactly = 0) { sdkManager.fetchAuthenticationConfiguration(any(), any()) } + verify(exactly = 0) { sdkManager.fetchAuthenticationConfiguration(any(), any(), any()) } } @Test @@ -1300,7 +1320,7 @@ class LoginViewModelTest { assert(viewModel.previousPendingServer == exampleUrl) assert(viewModel.selectedServer.value == exampleUrl) - verify(exactly = 0) { sdkManager.fetchAuthenticationConfiguration(any(), any()) } + verify(exactly = 0) { sdkManager.fetchAuthenticationConfiguration(any(), any(), any()) } } @Test @@ -1308,7 +1328,7 @@ class LoginViewModelTest { val sdkManager = mockk(relaxed = true) val callbackSlot = slot<() -> Unit>() - every { sdkManager.fetchAuthenticationConfiguration(any(), capture(callbackSlot)) } answers { + every { sdkManager.fetchAuthenticationConfiguration(any(), any(), capture(callbackSlot)) } answers { callbackSlot.captured.invoke() mockk() } @@ -1320,7 +1340,13 @@ class LoginViewModelTest { assert(viewModel.previousPendingServer == exampleUrl) assert(viewModel.selectedServer.value == exampleUrl) - verify(exactly = 1) { sdkManager.fetchAuthenticationConfiguration(any(), any()) } + verify(exactly = 1) { + sdkManager.fetchAuthenticationConfiguration( + httpAccess = any(), + loginServerUrl = exampleUrl, + completion = any(), + ) + } verify(exactly = 1) { job.cancel() } } 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 e093d9872a..1e96ba1f7d 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt @@ -46,7 +46,6 @@ import com.salesforce.androidsdk.ui.LoginActivity.Companion.isSalesforceWelcomeD import com.salesforce.androidsdk.ui.LoginActivity.Companion.SimulatedDiscoveryResult import com.salesforce.androidsdk.ui.LoginActivity.Companion.startDefaultLoginWithHintAndHost import com.salesforce.androidsdk.app.SalesforceSDKManager -import com.salesforce.androidsdk.config.LoginServerManager import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -393,5 +392,68 @@ class LoginActivityTest { } } + @Test + fun applySalesforceWelcomeLoginHintAndHost_setsPendingServer_andDoesNotPersistToLoginServerManager() { + val loginHost = "acme.my.salesforce.com" + val loginHint = "user@acme.com" + val expectedLoginUrl = "https://$loginHost" + + val intent = mockk(relaxed = true) + every { intent.getStringExtra(EXTRA_KEY_LOGIN_HINT) } returns loginHint + every { intent.getStringExtra(EXTRA_KEY_LOGIN_HOST) } returns loginHost + + val pendingServer = mockk>(relaxed = true) + val viewModel = mockk(relaxed = true) + every { viewModel.pendingServer } returns pendingServer + every { viewModel.loginHint = any() } just Runs + + val sdkManager = SalesforceSDKManager.getInstance() + val originalSelectedServer = sdkManager.loginServerManager.selectedLoginServer + val originalServersCount = sdkManager.loginServerManager.loginServers.size + + val activity = mockk(relaxed = true) + every { activity.viewModel } returns viewModel + every { activity.applySalesforceWelcomeLoginHintAndHost(intent) } answers { callOriginal() } + + activity.applySalesforceWelcomeLoginHintAndHost(intent) + + verify(exactly = 1) { viewModel.loginHint = loginHint } + verify(exactly = 1) { pendingServer.value = expectedLoginUrl } + org.junit.Assert.assertEquals( + originalSelectedServer, + sdkManager.loginServerManager.selectedLoginServer + ) + org.junit.Assert.assertEquals( + originalServersCount, + sdkManager.loginServerManager.loginServers.size + ) + org.junit.Assert.assertNull( + sdkManager.loginServerManager.getLoginServerFromURL(expectedLoginUrl) + ) + } + + @Test + fun applySalesforceWelcomeLoginHintAndHost_withoutLoginHostExtra_doesNotTouchPendingServer() { + val loginHint = "user@acme.com" + + val intent = mockk(relaxed = true) + every { intent.getStringExtra(EXTRA_KEY_LOGIN_HINT) } returns loginHint + every { intent.getStringExtra(EXTRA_KEY_LOGIN_HOST) } returns null + + val pendingServer = mockk>(relaxed = true) + val viewModel = mockk(relaxed = true) + every { viewModel.pendingServer } returns pendingServer + every { viewModel.loginHint = any() } just Runs + + val activity = mockk(relaxed = true) + every { activity.viewModel } returns viewModel + every { activity.applySalesforceWelcomeLoginHintAndHost(intent) } answers { callOriginal() } + + activity.applySalesforceWelcomeLoginHintAndHost(intent) + + verify(exactly = 1) { viewModel.loginHint = loginHint } + verify(exactly = 0) { pendingServer.value = any() } + } + // endregion } From 52c420d3224ddd60d22aa0fddb0d14275efe22c6 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 29 May 2026 17:17:09 -0700 Subject: [PATCH 2/3] Fix test failure. --- .../ui/LoginActivityScenarioTest.kt | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) 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 c37600f18a..7ad53edd31 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt @@ -27,7 +27,6 @@ package com.salesforce.androidsdk.ui import android.content.Intent -import android.net.Uri.parse import android.webkit.WebView import androidx.core.net.toUri import androidx.lifecycle.Lifecycle.State.RESUMED @@ -44,6 +43,7 @@ import com.salesforce.androidsdk.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HOST import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -52,26 +52,33 @@ import org.junit.runner.RunWith class LoginActivityScenarioTest { @Test - fun viewModelLoginHint_UpdatesOn_onCreateWithSalesforceWelcomeLoginHintIntentExtras() { + fun viewModelLoginHint_UpdatesOn_onNewIntentWithSalesforceWelcomeLoginHintIntentExtras() { val expectedLoginHint = "ietf_example_domain_reserved_for_test@example.com" val expectedLoginServerHostname = "welcome.salesforce.com" + val expectedPendingServer = "https://$expectedLoginServerHostname" + val loginServerManager = SalesforceSDKManager.getInstance().loginServerManager + val originalSelectedServer = loginServerManager.selectedLoginServer + + // Production never receives the welcome login hint+host extras as the initial + // launch intent: they are dispatched via [LoginActivity.startDefaultLoginWithHintAndHost] + // (internal, FLAG_ACTIVITY_SINGLE_TOP) onto the running LoginActivity, so the + // extras always arrive through onNewIntent. Mirror that here. launch( - Intent( - getApplicationContext(), - LoginActivity::class.java - ).apply { + Intent(getApplicationContext(), LoginActivity::class.java) + ).use { activityScenario -> + + val newIntent = Intent(getApplicationContext(), LoginActivity::class.java).apply { putExtra(EXTRA_KEY_LOGIN_HINT, expectedLoginHint) putExtra(EXTRA_KEY_LOGIN_HOST, expectedLoginServerHostname) - }).use { activityScenario -> + } + activityScenario.onActivity { activity -> activity.onNewIntent(newIntent) } activityScenario.onActivity { activity -> - - val actualLoginHint = activity.viewModel.loginHint - val actualLoginServerHostname = SalesforceSDKManager.getInstance().loginServerManager.selectedLoginServer - - assertEquals(expectedLoginHint, actualLoginHint) - assertEquals(expectedLoginServerHostname, parse(actualLoginServerHostname.url).host) + assertEquals(expectedLoginHint, activity.viewModel.loginHint) + assertEquals(expectedPendingServer, activity.viewModel.pendingServer.value) + assertEquals(originalSelectedServer, loginServerManager.selectedLoginServer) + assertNull(loginServerManager.getLoginServerFromURL(expectedPendingServer)) } } } From 62c68c38760efc77f411989aae361496211bf11a Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 29 May 2026 17:45:03 -0700 Subject: [PATCH 3/3] no message --- .../androidsdk/ui/LoginActivityScenarioTest.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 7ad53edd31..fe75d2f21e 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt @@ -52,7 +52,7 @@ import org.junit.runner.RunWith class LoginActivityScenarioTest { @Test - fun viewModelLoginHint_UpdatesOn_onNewIntentWithSalesforceWelcomeLoginHintIntentExtras() { + fun viewModelLoginHint_UpdatesOn_applyWelcomeLoginHintAndHostIntentExtras() { val expectedLoginHint = "ietf_example_domain_reserved_for_test@example.com" val expectedLoginServerHostname = "welcome.salesforce.com" val expectedPendingServer = "https://$expectedLoginServerHostname" @@ -62,17 +62,20 @@ class LoginActivityScenarioTest { // Production never receives the welcome login hint+host extras as the initial // launch intent: they are dispatched via [LoginActivity.startDefaultLoginWithHintAndHost] - // (internal, FLAG_ACTIVITY_SINGLE_TOP) onto the running LoginActivity, so the - // extras always arrive through onNewIntent. Mirror that here. + // (internal, FLAG_ACTIVITY_SINGLE_TOP) onto the running LoginActivity and arrive + // through onNewIntent, which calls applySalesforceWelcomeLoginHintAndHost. Drive + // that function directly here since onNewIntent itself is protected. launch( Intent(getApplicationContext(), LoginActivity::class.java) ).use { activityScenario -> - val newIntent = Intent(getApplicationContext(), LoginActivity::class.java).apply { + val intentWithExtras = Intent(getApplicationContext(), LoginActivity::class.java).apply { putExtra(EXTRA_KEY_LOGIN_HINT, expectedLoginHint) putExtra(EXTRA_KEY_LOGIN_HOST, expectedLoginServerHostname) } - activityScenario.onActivity { activity -> activity.onNewIntent(newIntent) } + activityScenario.onActivity { activity -> + activity.applySalesforceWelcomeLoginHintAndHost(intentWithExtras) + } activityScenario.onActivity { activity -> assertEquals(expectedLoginHint, activity.viewModel.loginHint)