diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/ManageSpaceActivity.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/ManageSpaceActivity.kt index 013b32309c..4736daa5fc 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/ManageSpaceActivity.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/ManageSpaceActivity.kt @@ -53,6 +53,7 @@ import com.salesforce.androidsdk.R.string.sf__manage_space_confirmation import com.salesforce.androidsdk.R.string.sf__manage_space_logout_no import com.salesforce.androidsdk.R.string.sf__manage_space_logout_yes import com.salesforce.androidsdk.R.string.sf__manage_space_title +import com.salesforce.androidsdk.accounts.UserAccount import com.salesforce.androidsdk.app.SalesforceSDKManager.Companion.getInstance import com.salesforce.androidsdk.auth.OAuth2.LogoutReason.USER_LOGOUT @@ -97,11 +98,12 @@ open class ManageSpaceActivity : ComponentActivity() { finish() }, onConfirm = { - getInstance().logout( - account = null, - frontActivity = this@ManageSpaceActivity, - showLoginPage = false, - reason = USER_LOGOUT + val sdkManager = getInstance() + logoutAllUsersAndClearData( + authenticatedUsers = sdkManager.userAccountManager.authenticatedUsers, + signoutUser = { user -> sdkManager.userAccountManager.signoutUser(user, this@ManageSpaceActivity, false, USER_LOGOUT) }, + fallbackLogout = { sdkManager.logout(account = null, frontActivity = this@ManageSpaceActivity, showLoginPage = false, reason = USER_LOGOUT) }, + clearData = { clearApplicationUserData() } ) }, titleText = stringResource(sf__manage_space_title), @@ -173,4 +175,37 @@ open class ManageSpaceActivity : ComponentActivity() { } }) } + + // Extracted to allow overriding in tests — clearApplicationUserData() kills the process. + open fun clearApplicationUserData() { + (getSystemService(android.content.Context.ACTIVITY_SERVICE) as android.app.ActivityManager) + .clearApplicationUserData() + } +} + +/** + * Logs out all authenticated users and then calls [clearData]. + * + * Extracted as a top-level function so it can be unit-tested without launching the full + * Activity (Activity constructors require a Looper) or mocking framework singletons. + * All side effects are injected as lambdas. + * + * @param authenticatedUsers the currently logged-in accounts, or null/empty if none + * @param signoutUser called once per user to perform SDK-level per-account logout + * @param fallbackLogout called when [authenticatedUsers] is null/empty — handles the + * fresh-install or already-logged-out edge case via the standard SDK logout path + * @param clearData called last to wipe all app storage (normally [ManageSpaceActivity.clearApplicationUserData]) + */ +internal fun logoutAllUsersAndClearData( + authenticatedUsers: List?, + signoutUser: (UserAccount) -> Unit, + fallbackLogout: () -> Unit, + clearData: () -> Unit, +) { + if (authenticatedUsers.isNullOrEmpty()) { + fallbackLogout() + } else { + authenticatedUsers.forEach { user -> signoutUser(user) } + } + clearData() } diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/ManageSpaceActivityTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/ManageSpaceActivityTest.kt new file mode 100644 index 0000000000..803b9d17ed --- /dev/null +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/ManageSpaceActivityTest.kt @@ -0,0 +1,140 @@ +/* + * 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.ui + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.salesforce.androidsdk.accounts.UserAccount +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [logoutAllUsersAndClearData]. + * + * All dependencies are passed as lambdas, so no Android framework objects or + * SDK singletons need to be mocked — the tests run in the instrumentation + * process without launching any Activity. + */ +@RunWith(AndroidJUnit4::class) +@SmallTest +class ManageSpaceActivityTest { + + // ------------------------------------------------------------------------- + // Tests + // ------------------------------------------------------------------------- + + @Test + fun test_givenSingleUser_whenConfirm_thenSignoutCalledOnceAndClearDataCalled() { + val user = createUser("user1") + val signedOut = mutableListOf() + var fallbackCalled = false + var clearDataCalled = false + + logoutAllUsersAndClearData( + authenticatedUsers = listOf(user), + signoutUser = { signedOut.add(it) }, + fallbackLogout = { fallbackCalled = true }, + clearData = { clearDataCalled = true }, + ) + + assertEquals("signoutUser must be called exactly once", listOf(user), signedOut) + assertTrue("clearApplicationUserData must be called", clearDataCalled) + assertTrue("fallback logout must not be called with a known user", !fallbackCalled) + } + + @Test + fun test_givenMultipleUsers_whenConfirm_thenSignoutCalledForEachUserAndClearDataCalled() { + val users = listOf(createUser("user1"), createUser("user2"), createUser("user3")) + val signedOut = mutableListOf() + var clearDataCalled = false + + logoutAllUsersAndClearData( + authenticatedUsers = users, + signoutUser = { signedOut.add(it) }, + fallbackLogout = {}, + clearData = { clearDataCalled = true }, + ) + + assertEquals("signoutUser must be called for each user", users, signedOut) + assertTrue("clearApplicationUserData must be called", clearDataCalled) + } + + @Test + fun test_givenNoUsers_whenConfirm_thenFallbackLogoutCalledAndClearDataCalled() { + val signedOut = mutableListOf() + var fallbackCalled = false + var clearDataCalled = false + + logoutAllUsersAndClearData( + authenticatedUsers = null, + signoutUser = { signedOut.add(it) }, + fallbackLogout = { fallbackCalled = true }, + clearData = { clearDataCalled = true }, + ) + + assertTrue("signoutUser must not be called when no users", signedOut.isEmpty()) + assertTrue("fallback logout must be called when no users", fallbackCalled) + assertTrue("clearApplicationUserData must be called", clearDataCalled) + } + + @Test + fun test_givenEmptyUserList_whenConfirm_thenFallbackLogoutCalledAndClearDataCalled() { + val signedOut = mutableListOf() + var fallbackCalled = false + var clearDataCalled = false + + logoutAllUsersAndClearData( + authenticatedUsers = emptyList(), + signoutUser = { signedOut.add(it) }, + fallbackLogout = { fallbackCalled = true }, + clearData = { clearDataCalled = true }, + ) + + assertTrue("signoutUser must not be called for empty list", signedOut.isEmpty()) + assertTrue("fallback logout must be called for empty list", fallbackCalled) + assertTrue("clearApplicationUserData must be called", clearDataCalled) + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private fun createUser(userId: String): UserAccount = + com.salesforce.androidsdk.accounts.UserAccountBuilder.getInstance() + .authToken("token") + .refreshToken("refresh") + .loginServer("https://test.salesforce.com") + .idUrl("https://test.salesforce.com/org/$userId") + .instanceServer("https://cs1.salesforce.com") + .orgId("org") + .userId(userId) + .username("$userId@example.com") + .accountName("$userId (https://cs1.salesforce.com) (SalesforceSDKTest)") + .build() +}