mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 18:26:31 -05:00
BIT-853: Implement account switching (#316)
This commit is contained in:
@@ -30,6 +30,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
@@ -1079,6 +1080,106 @@ class AuthRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `switchAccount when there is no saved UserState should do nothing`() {
|
||||
val updatedUserId = USER_ID_2
|
||||
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertNull(repository.userStateFlow.value)
|
||||
|
||||
assertEquals(
|
||||
SwitchAccountResult.NoChange,
|
||||
repository.switchAccount(userId = updatedUserId),
|
||||
)
|
||||
|
||||
assertNull(repository.userStateFlow.value)
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `switchAccount when the given userId is the same as the current activeUserId should do nothing`() {
|
||||
val originalUserId = USER_ID_1
|
||||
val originalUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_STATE,
|
||||
specialCircumstance = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
SwitchAccountResult.NoChange,
|
||||
repository.switchAccount(userId = originalUserId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `switchAccount when the given userId does not correspond to a saved account should do nothing`() {
|
||||
val originalUserId = USER_ID_1
|
||||
val invalidId = "invalidId"
|
||||
val originalUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_STATE,
|
||||
specialCircumstance = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
SwitchAccountResult.NoChange,
|
||||
repository.switchAccount(userId = invalidId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `switchAccount when the userId is valid should update the current UserState, lock the vault of the previous active user, and clear the previously unlocked data`() {
|
||||
val originalUserId = USER_ID_1
|
||||
val updatedUserId = USER_ID_2
|
||||
val originalUserState = MULTI_USER_STATE.toUserState(
|
||||
vaultState = VAULT_STATE,
|
||||
specialCircumstance = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
SwitchAccountResult.AccountSwitched,
|
||||
repository.switchAccount(userId = updatedUserId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
originalUserState.copy(activeUserId = updatedUserId),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
verify { vaultRepository.lockVaultIfNecessary(originalUserId) }
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasswordBreachCount should return failure when service returns failure`() = runTest {
|
||||
val password = "password"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@@ -36,6 +36,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } just runs
|
||||
every { logout() } just runs
|
||||
every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
|
||||
@@ -163,15 +164,17 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SwitchAccountClick should emit ShowToast`() = runTest {
|
||||
fun `on SwitchAccountClick should switch to the given account`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val accountSummary = mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.ACTIVE
|
||||
}
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultUnlockAction.SwitchAccountClick(accountSummary))
|
||||
assertEquals(VaultUnlockEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
|
||||
}
|
||||
val updatedUserId = "updatedUserId"
|
||||
viewModel.trySendAction(
|
||||
VaultUnlockAction.SwitchAccountClick(
|
||||
accountSummary = mockk {
|
||||
every { userId } returns updatedUserId
|
||||
},
|
||||
),
|
||||
)
|
||||
verify { authRepository.switchAccount(userId = updatedUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
@@ -30,11 +31,14 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
private val mutableVaultDataStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
|
||||
private var switchAccountResult: SwitchAccountResult = SwitchAccountResult.NoChange
|
||||
|
||||
private val authRepository: AuthRepository =
|
||||
mockk {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } just runs
|
||||
every { switchAccount(any()) } answers { switchAccountResult }
|
||||
}
|
||||
|
||||
private val vaultRepository: VaultRepository =
|
||||
@@ -52,30 +56,70 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `UserState updates with a null value should do nothing`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value = null
|
||||
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserState updates with a non-null value update the account information in the state`() {
|
||||
fun `UserState updates with a non-null value when switching accounts should do nothing`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "Other User",
|
||||
email = "active@bitwarden.com",
|
||||
avatarColorHex = "#00aaaa",
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
),
|
||||
// Ensure we are currently switching accounts
|
||||
val initialState = DEFAULT_STATE.copy(isSwitchingAccounts = true)
|
||||
switchAccountResult = SwitchAccountResult.AccountSwitched
|
||||
val updatedUserId = "lockedUserId"
|
||||
viewModel.trySendAction(
|
||||
VaultAction.AccountSwitchClick(
|
||||
accountSummary = mockk() {
|
||||
every { userId } returns updatedUserId
|
||||
},
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
initialState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(activeUserId = updatedUserId)
|
||||
|
||||
assertEquals(
|
||||
initialState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserState updates with a non-null value when not switching accounts should update the account information in the state`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
mutableUserStateFlow.value =
|
||||
DEFAULT_USER_STATE.copy(
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "Other User",
|
||||
email = "active@bitwarden.com",
|
||||
avatarColorHex = "#00aaaa",
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
@@ -95,50 +139,47 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AccountSwitchClick for the active account should do nothing`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
fun `on AccountSwitchClick when result is NoChange should try to switch to the given account and set isSwitchingAccounts to false`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
switchAccountResult = SwitchAccountResult.NoChange
|
||||
val updatedUserId = "lockedUserId"
|
||||
viewModel.trySendAction(
|
||||
VaultAction.AccountSwitchClick(
|
||||
accountSummary = mockk {
|
||||
every { status } returns AccountSummary.Status.ACTIVE
|
||||
every { userId } returns updatedUserId
|
||||
},
|
||||
),
|
||||
)
|
||||
expectNoEvents()
|
||||
verify { authRepository.switchAccount(userId = updatedUserId) }
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isSwitchingAccounts = false),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AccountSwitchClick for a locked account emit NavigateToVaultUnlockScreen`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
fun `on AccountSwitchClick when result is AccountSwitched should switch to the given account and set isSwitchingAccounts to true`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
switchAccountResult = SwitchAccountResult.AccountSwitched
|
||||
val updatedUserId = "lockedUserId"
|
||||
viewModel.trySendAction(
|
||||
VaultAction.AccountSwitchClick(
|
||||
accountSummary = mockk {
|
||||
every { status } returns AccountSummary.Status.LOCKED
|
||||
every { userId } returns updatedUserId
|
||||
},
|
||||
),
|
||||
)
|
||||
assertEquals(VaultEvent.NavigateToVaultUnlockScreen, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on AccountSwitchClick for an unlocked account emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.AccountSwitchClick(
|
||||
accountSummary = mockk {
|
||||
every { status } returns AccountSummary.Status.UNLOCKED
|
||||
},
|
||||
),
|
||||
verify { authRepository.switchAccount(userId = updatedUserId) }
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isSwitchingAccounts = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
assertEquals(VaultEvent.ShowToast("Not yet implemented."), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
@@ -257,6 +298,39 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow updates should do nothing when switching accounts`() {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
// Ensure we are currently switching accounts
|
||||
val initialState = DEFAULT_STATE.copy(isSwitchingAccounts = true)
|
||||
switchAccountResult = SwitchAccountResult.AccountSwitched
|
||||
val updatedUserId = "lockedUserId"
|
||||
viewModel.trySendAction(
|
||||
VaultAction.AccountSwitchClick(
|
||||
accountSummary = mockk() {
|
||||
every { userId } returns updatedUserId
|
||||
},
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
initialState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
initialState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddItemClick should emit NavigateToAddItemScreen`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -367,6 +441,14 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
),
|
||||
UserState.Account(
|
||||
userId = "lockedUserId",
|
||||
name = "Locked User",
|
||||
email = "locked@bitwarden.com",
|
||||
avatarColorHex = "#00aaaa",
|
||||
isPremium = false,
|
||||
isVaultUnlocked = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -382,6 +464,14 @@ private fun createMockVaultState(viewState: VaultState.ViewState): VaultState =
|
||||
avatarColorHex = "#aa00aa",
|
||||
status = AccountSummary.Status.ACTIVE,
|
||||
),
|
||||
AccountSummary(
|
||||
userId = "lockedUserId",
|
||||
name = "Locked User",
|
||||
email = "locked@bitwarden.com",
|
||||
avatarColorHex = "#00aaaa",
|
||||
status = AccountSummary.Status.LOCKED,
|
||||
),
|
||||
),
|
||||
viewState = viewState,
|
||||
isSwitchingAccounts = false,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user