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/LoginActivityScenarioTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt index c37600f18a..fe75d2f21e 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,36 @@ import org.junit.runner.RunWith class LoginActivityScenarioTest { @Test - fun viewModelLoginHint_UpdatesOn_onCreateWithSalesforceWelcomeLoginHintIntentExtras() { + fun viewModelLoginHint_UpdatesOn_applyWelcomeLoginHintAndHostIntentExtras() { 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 and arrive + // through onNewIntent, which calls applySalesforceWelcomeLoginHintAndHost. Drive + // that function directly here since onNewIntent itself is protected. launch( - Intent( - getApplicationContext(), - LoginActivity::class.java - ).apply { + Intent(getApplicationContext(), LoginActivity::class.java) + ).use { activityScenario -> + + val intentWithExtras = Intent(getApplicationContext(), LoginActivity::class.java).apply { putExtra(EXTRA_KEY_LOGIN_HINT, expectedLoginHint) putExtra(EXTRA_KEY_LOGIN_HOST, expectedLoginServerHostname) - }).use { activityScenario -> - + } activityScenario.onActivity { activity -> + activity.applySalesforceWelcomeLoginHintAndHost(intentWithExtras) + } - val actualLoginHint = activity.viewModel.loginHint - val actualLoginServerHostname = SalesforceSDKManager.getInstance().loginServerManager.selectedLoginServer - - assertEquals(expectedLoginHint, actualLoginHint) - assertEquals(expectedLoginServerHostname, parse(actualLoginServerHostname.url).host) + activityScenario.onActivity { activity -> + assertEquals(expectedLoginHint, activity.viewModel.loginHint) + assertEquals(expectedPendingServer, activity.viewModel.pendingServer.value) + assertEquals(originalSelectedServer, loginServerManager.selectedLoginServer) + assertNull(loginServerManager.getLoginServerFromURL(expectedPendingServer)) } } } 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 }