Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(""))
Expand Down Expand Up @@ -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
Expand All @@ -1300,15 +1320,15 @@ 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
fun loginViewModel_applyPendingLoginServer_setsSelectedServerWithFetchedAuthConfig() {

val sdkManager = mockk<SalesforceSDKManager>(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<Job>()
}
Expand All @@ -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() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<LoginActivity>(
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))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Intent>(relaxed = true)
every { intent.getStringExtra(EXTRA_KEY_LOGIN_HINT) } returns loginHint
every { intent.getStringExtra(EXTRA_KEY_LOGIN_HOST) } returns loginHost

val pendingServer = mockk<MediatorLiveData<String>>(relaxed = true)
val viewModel = mockk<LoginViewModel>(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<LoginActivity>(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<Intent>(relaxed = true)
every { intent.getStringExtra(EXTRA_KEY_LOGIN_HINT) } returns loginHint
every { intent.getStringExtra(EXTRA_KEY_LOGIN_HOST) } returns null

val pendingServer = mockk<MediatorLiveData<String>>(relaxed = true)
val viewModel = mockk<LoginViewModel>(relaxed = true)
every { viewModel.pendingServer } returns pendingServer
every { viewModel.loginHint = any() } just Runs

val activity = mockk<LoginActivity>(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
}
Loading