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
7 changes: 7 additions & 0 deletions libs/SalesforceSDK/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@
android:theme="@style/SalesforceSDK"
android:exported="false" />

<!-- Test Authentication Activity For Automated Testing. This activity is only functional for debug builds of the app using Salesforce Mobile SDK -->
<activity
android:name="com.salesforce.androidsdk.util.test.TestAuthenticationActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@style/SalesforceSDK" />

<!-- Receiver in SP app for IDP-SP login flows -->
<receiver android:name="com.salesforce.androidsdk.auth.idp.SPReceiver"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ fun DevInfoScreen(
}

@Composable
fun DevInfoItem(name: String, value: String) {
fun DevInfoItem(name: String, value: String?) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text(text = name, fontWeight = FontWeight.Bold)
Text(text = value, color = Color.Gray)
Text(text = value ?: "", color = Color.Gray)
HorizontalDivider()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2025-present, salesforce.com, inc.
* All rights reserved.
* Redistribution and use of this software in source and binary forms, with or
* without modification, are permitted provided that the following conditions
* are met:
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of salesforce.com, inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission of salesforce.com, inc.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

package com.salesforce.androidsdk.util.test

import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.salesforce.androidsdk.accounts.UserAccountBuilder
import com.salesforce.androidsdk.accounts.UserAccountManager.USER_SWITCH_TYPE_DEFAULT
import com.salesforce.androidsdk.accounts.UserAccountManager.USER_SWITCH_TYPE_FIRST_LOGIN
import com.salesforce.androidsdk.accounts.UserAccountManager.USER_SWITCH_TYPE_LOGIN
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.rest.ClientManager.AccMgrAuthTokenProvider
import com.salesforce.androidsdk.util.test.TestCredentials.ACCOUNT_NAME
import com.salesforce.androidsdk.util.test.TestCredentials.CLIENT_ID
import com.salesforce.androidsdk.util.test.TestCredentials.COMMUNITY_URL
import com.salesforce.androidsdk.util.test.TestCredentials.IDENTITY_URL
import com.salesforce.androidsdk.util.test.TestCredentials.INSTANCE_URL
import com.salesforce.androidsdk.util.test.TestCredentials.LANGUAGE
import com.salesforce.androidsdk.util.test.TestCredentials.LOCALE
import com.salesforce.androidsdk.util.test.TestCredentials.LOGIN_URL
import com.salesforce.androidsdk.util.test.TestCredentials.ORG_ID
import com.salesforce.androidsdk.util.test.TestCredentials.PHOTO_URL
import com.salesforce.androidsdk.util.test.TestCredentials.REFRESH_TOKEN
import com.salesforce.androidsdk.util.test.TestCredentials.USERNAME
import com.salesforce.androidsdk.util.test.TestCredentials.USER_ID

/**
* An activity that authenticates using credentials provided in the intent
* rather than user interaction. This is intended only for test automation in
* app debug build variants. This class should not be used in release builds as
* it will simply finish without any action.
*/
class TestAuthenticationActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val credentials = intent.getStringExtra("creds")
if (!SalesforceSDKManager.getInstance().isDebugBuild || credentials == null) {
finish()
return
}

TestCredentials.init(credentials, this)

val account = UserAccountBuilder.getInstance()
.refreshToken(REFRESH_TOKEN)
.instanceServer(INSTANCE_URL)
.idUrl(IDENTITY_URL)
.orgId(ORG_ID)
.userId(USER_ID)
.communityUrl(COMMUNITY_URL)
.accountName(ACCOUNT_NAME)
.clientId(CLIENT_ID)
.photoUrl(PHOTO_URL)
.language(LANGUAGE)
.locale(LOCALE)
.loginServer(LOGIN_URL)
.authToken("Nothing yet")
.firstName("User")
.lastName("Test")
.displayName("Test user")
.username(USERNAME)
.build()

val authTokenProvider = AccMgrAuthTokenProvider(
SalesforceSDKManager.getInstance().clientManager,
INSTANCE_URL,
null,
REFRESH_TOKEN
)
authTokenProvider.newAuthToken
account.downloadProfilePhoto()

val userAccountManager = SalesforceSDKManager.getInstance().userAccountManager
// Send user switch intent, create and switch to user.
val numAuthenticatedUsers = userAccountManager.authenticatedUsers?.size ?: 0
val userSwitchType = when {
// We've already authenticated the first user, so there should be one.
numAuthenticatedUsers == 1 -> USER_SWITCH_TYPE_FIRST_LOGIN

// Otherwise we're logging in with an additional user.
numAuthenticatedUsers > 1 -> USER_SWITCH_TYPE_LOGIN

// This should never happen but if it does, pass in the "unknown" value.
else -> USER_SWITCH_TYPE_DEFAULT
}
userAccountManager.sendUserSwitchIntent(userSwitchType, null)
userAccountManager.createAccount(account)
userAccountManager.switchToUser(account)

startActivity(Intent(this, SalesforceSDKManager.getInstance().mainActivityClass).apply {
setPackage(SalesforceSDKManager.getInstance().appContext.packageName)
flags = FLAG_ACTIVITY_NEW_TASK
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@
import com.salesforce.androidsdk.util.JSONObjectHelper;
import com.salesforce.androidsdk.util.ResourceReaderHelper;

import org.json.JSONException;
import org.json.JSONObject;

/**
* Authentication credentials used to make live server calls in tests
*
* <p>
* To populate test_credentials.json clone SalesforceMobileSDK-Shared and run web app in credsHelper folder
*/
public class TestCredentials {
Expand Down Expand Up @@ -79,9 +80,32 @@ public static void init(Context ctx) {
PHOTO_URL = json.getString("photo_url");
LANGUAGE = json.optString("language", "en_US");
LOCALE = json.optString("locale", "en_US");
}
catch (Exception e) {
} catch (Exception e) {
throw new RuntimeException("Failed to read test_credentials.json", e);
}
}

public static void init(String creds, Context ctx) {
try {
JSONObject json = new JSONObject(creds);
API_VERSION = ApiVersionStrings.getVersionNumber(ctx);
ACCOUNT_TYPE = ctx.getString(R.string.account_type);
ORG_ID = json.getString("organization_id");
USERNAME = json.getString("username");
ACCOUNT_NAME = json.getString("display_name");
USER_ID = json.getString("user_id");
LOGIN_URL = json.getString("test_login_domain");
INSTANCE_URL = json.getString("instance_url");
COMMUNITY_URL = json.optString("community_url", INSTANCE_URL /* In case the test_credentials.json was obtained for a user/org without community setup */);
IDENTITY_URL = json.getString("identity_url");
CLIENT_ID = json.getString("test_client_id");
REFRESH_TOKEN = json.getString("refresh_token");
PHOTO_URL = json.getString("photo_url");
LANGUAGE = json.optString("language", "en_US");
LOCALE = json.optString("locale", "en_US");

} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
Expand Down Expand Up @@ -118,11 +119,10 @@ class LoginViewActivityTest {

@Test
fun topAppBar_ChangeServerButton_OpensServerPicker() {
var showPicker: MutableState<Boolean>? = null
val showPicker = mutableStateOf(false)
Copy link
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This subtle change resolves inspector warnings and should be acceptable for tests. I'd used the same style where dynamic background color is now used as well.

androidComposeTestRule.setContent {
showPicker = remember { mutableStateOf(false) }
DefaultTopAppBarTestWrapper(
showServerPicker = showPicker!!
showServerPicker = showPicker
)
}

Expand All @@ -143,10 +143,10 @@ class LoginViewActivityTest {

menu.performClick()
changeServerButton.assertIsDisplayed()
Assert.assertFalse("Picker should not be shown yet.", showPicker!!.value)
Assert.assertFalse("Picker should not be shown yet.", showPicker.value)

changeServerButton.performClick()
Assert.assertTrue("Picker should be shown.", showPicker!!.value)
Assert.assertTrue("Picker should be shown.", showPicker.value)
}

@Test
Expand Down Expand Up @@ -284,8 +284,10 @@ class LoginViewActivityTest {

@Test
fun loginView_DefaultComponents_DisplayCorrectly() {
val dynamicBackgroundColor = mutableStateOf(White)
androidComposeTestRule.setContent {
LoginViewTestWrapper(
dynamicBackgroundColor = dynamicBackgroundColor,
topAppBar = {
DefaultTopAppBarTestWrapper(shouldShowBackButton = true)
},
Expand Down Expand Up @@ -317,8 +319,10 @@ class LoginViewActivityTest {

@Test
fun loginView_Loading_DisplayCorrectly() {
val dynamicBackgroundColor = mutableStateOf(White)
androidComposeTestRule.setContent {
LoginViewTestWrapper(
dynamicBackgroundColor = dynamicBackgroundColor,
topAppBar = {
DefaultTopAppBarTestWrapper(shouldShowBackButton = true)
},
Expand Down Expand Up @@ -371,8 +375,10 @@ class LoginViewActivityTest {
)
}

val dynamicBackgroundColor = mutableStateOf(White)
androidComposeTestRule.setContent {
LoginViewTestWrapper(
dynamicBackgroundColor = dynamicBackgroundColor,
topAppBar = customTopAppBar,
loading = true,
loadingIndicator = customLoadingIndicator,
Expand All @@ -394,7 +400,7 @@ class LoginViewActivityTest {
*/
@Composable
private fun DefaultTopAppBarTestWrapper(
backgroundColor: Color = Color.White,
backgroundColor: Color = White,
titleText: String = DEFAULT_URL,
titleTextColor: Color = Color.Black,
showServerPicker: MutableState<Boolean> = remember { mutableStateOf(false) },
Expand All @@ -415,7 +421,7 @@ class LoginViewActivityTest {
*/
@Composable
private fun DefaultBottomAppBarTestWrapper(
backgroundColor: MutableState<Color> = mutableStateOf(Color.White),
backgroundColor: MutableState<Color> = mutableStateOf(White),
button: LoginViewModel.BottomBarButton? = null,
loading: Boolean = false,
showButton: Boolean = true,
Expand All @@ -428,6 +434,7 @@ class LoginViewActivityTest {
*/
@Composable
private fun LoginViewTestWrapper(
dynamicBackgroundColor: MutableState<Color>,
loginUrlData: LiveData<String> = liveData { DEFAULT_URL },
topAppBar: @Composable () -> Unit = { DefaultTopAppBarTestWrapper() },
webView: WebView = WebView(LocalContext.current),
Expand All @@ -436,6 +443,6 @@ class LoginViewActivityTest {
bottomAppBar: @Composable () -> Unit = { DefaultBottomAppBarTestWrapper() },
showServerPicker: MutableState<Boolean> = mutableStateOf(false),
) {
LoginView(loginUrlData, topAppBar, webView, loading, loadingIndicator, bottomAppBar, showServerPicker)
LoginView(dynamicBackgroundColor, loginUrlData, topAppBar, webView, loading, loadingIndicator, bottomAppBar, showServerPicker)
}
}
}
Loading