mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-1019, BIT-1190: Add "Never Lock" vault timeout implementation (#559)
This commit is contained in:
parent
38cd8984e9
commit
125c304d12
16 changed files with 350 additions and 28 deletions
|
@ -5,6 +5,10 @@ import com.bitwarden.core.InitUserCryptoMethod
|
|||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
@ -12,21 +16,34 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResul
|
|||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Primary implementation [VaultLockManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class VaultLockManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
) : VaultLockManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
private val mutableVaultStateStateFlow =
|
||||
|
@ -40,6 +57,10 @@ class VaultLockManagerImpl(
|
|||
override val vaultStateFlow: StateFlow<VaultState>
|
||||
get() = mutableVaultStateStateFlow.asStateFlow()
|
||||
|
||||
init {
|
||||
observeVaultTimeoutChanges()
|
||||
}
|
||||
|
||||
override fun isVaultUnlocked(userId: String): Boolean =
|
||||
userId in mutableVaultStateStateFlow.value.unlockedVaultUserIds
|
||||
|
||||
|
@ -52,12 +73,16 @@ class VaultLockManagerImpl(
|
|||
|
||||
override fun lockVaultForCurrentUser() {
|
||||
activeUserId?.let {
|
||||
lockVaultIfNecessary(it)
|
||||
lockVault(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun lockVaultIfNecessary(userId: String) {
|
||||
// TODO: Check for VaultTimeout.Never (BIT-1019)
|
||||
// Don't lock the vault for users with a Never Lock timeout.
|
||||
val hasNeverLockTimeout =
|
||||
settingsRepository.getVaultTimeoutStateFlow(userId = userId).value == VaultTimeout.Never
|
||||
if (hasNeverLockTimeout) return
|
||||
|
||||
lockVault(userId = userId)
|
||||
}
|
||||
|
||||
|
@ -146,4 +171,89 @@ class VaultLockManagerImpl(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun observeVaultTimeoutChanges() {
|
||||
authDiskSource
|
||||
.userStateFlow
|
||||
.map { userState -> userState?.accounts?.keys.orEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { userIds ->
|
||||
userIds
|
||||
.map { userId -> vaultTimeoutChangesForUserFlow(userId = userId) }
|
||||
.merge()
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
private fun vaultTimeoutChangesForUserFlow(userId: String) =
|
||||
settingsRepository
|
||||
.getVaultTimeoutStateFlow(userId = userId)
|
||||
.onEach { vaultTimeout ->
|
||||
handleUserAutoUnlockChanges(
|
||||
userId = userId,
|
||||
vaultTimeout = vaultTimeout,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleUserAutoUnlockChanges(
|
||||
userId: String,
|
||||
vaultTimeout: VaultTimeout,
|
||||
) {
|
||||
if (vaultTimeout != VaultTimeout.Never) {
|
||||
// Clear the user encryption keys
|
||||
authDiskSource.storeUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (isVaultUnlocked(userId = userId)) {
|
||||
// Get and save the key if necessary
|
||||
val userAutoUnlockKey =
|
||||
vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
.getOrNull()
|
||||
authDiskSource.storeUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = userAutoUnlockKey,
|
||||
)
|
||||
} else {
|
||||
// Retrieve the key. If non-null, unlock the user
|
||||
authDiskSource.getUserAutoUnlockKey(userId = userId)?.let {
|
||||
val result = unlockVaultForUser(
|
||||
userId = userId,
|
||||
initUserCryptoMethod =
|
||||
InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = it,
|
||||
),
|
||||
)
|
||||
if (result is VaultUnlockResult.Success) {
|
||||
setVaultToUnlocked(userId = userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private suspend fun unlockVaultForUser(
|
||||
userId: String,
|
||||
initUserCryptoMethod: InitUserCryptoMethod,
|
||||
): VaultUnlockResult {
|
||||
val account = authDiskSource.userState?.accounts?.get(userId)
|
||||
?: return VaultUnlockResult.InvalidStateError
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError
|
||||
val organizationKeys = authDiskSource
|
||||
.getOrganizationKeys(userId = userId)
|
||||
return unlockVault(
|
||||
userId = userId,
|
||||
email = account.profile.email,
|
||||
kdf = account.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = initUserCryptoMethod,
|
||||
organizationKeys = organizationKeys,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.vault.manager.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl
|
||||
|
@ -22,9 +24,13 @@ object VaultManagerModule {
|
|||
fun provideVaultLockManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
settingsRepository: SettingsRepository,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): VaultLockManager =
|
||||
VaultLockManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsRepository = settingsRepository,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ class LandingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLockAccountClicked(action: LandingAction.LockAccountClick) {
|
||||
vaultRepository.lockVaultIfNecessary(userId = action.accountSummary.userId)
|
||||
vaultRepository.lockVault(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleLogoutAccountClicked(action: LandingAction.LogoutAccountClick) {
|
||||
|
|
|
@ -98,7 +98,7 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLockAccountClicked(action: LoginAction.LockAccountClick) {
|
||||
vaultRepository.lockVaultIfNecessary(userId = action.accountSummary.userId)
|
||||
vaultRepository.lockVault(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleLogoutAccountClicked(action: LoginAction.LogoutAccountClick) {
|
||||
|
|
|
@ -113,7 +113,7 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLockAccountClick(action: VaultUnlockAction.LockAccountClick) {
|
||||
vaultRepo.lockVaultIfNecessary(userId = action.accountSummary.userId)
|
||||
vaultRepo.lockVault(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleLogoutAccountClick(action: VaultUnlockAction.LogoutAccountClick) {
|
||||
|
@ -174,6 +174,9 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
// out.
|
||||
val userState = action.userState ?: return
|
||||
|
||||
// If the Vault is already unlocked, do nothing.
|
||||
if (userState.activeAccount.isVaultUnlocked) return
|
||||
|
||||
mutableStateFlow.update {
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
|
@ -23,6 +24,7 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAP
|
|||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph
|
||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph
|
||||
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Controls root level [NavHost] for the app.
|
||||
|
@ -34,6 +36,7 @@ fun RootNavScreen(
|
|||
onSplashScreenRemoved: () -> Unit = {},
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val previousStateReference = remember { AtomicReference(state) }
|
||||
|
||||
val isNotSplashScreen = state != RootNavState.Splash
|
||||
LaunchedEffect(isNotSplashScreen) {
|
||||
|
@ -58,15 +61,19 @@ fun RootNavScreen(
|
|||
RootNavState.Auth -> AUTH_GRAPH_ROUTE
|
||||
RootNavState.Splash -> SPLASH_ROUTE
|
||||
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
||||
RootNavState.VaultUnlocked -> VAULT_UNLOCKED_GRAPH_ROUTE
|
||||
is RootNavState.VaultUnlocked -> VAULT_UNLOCKED_GRAPH_ROUTE
|
||||
}
|
||||
val currentRoute = navController.currentDestination?.rootLevelRoute()
|
||||
|
||||
// Don't navigate if we are already at the correct root. This notably happens during process
|
||||
// death. In this case, the NavHost already restores state, so we don't have to navigate.
|
||||
if (currentRoute == targetRoute) {
|
||||
// However, if the route is correct but the underlying state is different, we should still
|
||||
// proceed in order to get a fresh version of that route.
|
||||
if (currentRoute == targetRoute && previousStateReference.get() == state) {
|
||||
previousStateReference.set(state)
|
||||
return
|
||||
}
|
||||
previousStateReference.set(state)
|
||||
|
||||
// When state changes, navigate to different root navigation state
|
||||
val rootNavOptions = navOptions {
|
||||
|
@ -83,7 +90,7 @@ fun RootNavScreen(
|
|||
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
||||
RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,12 @@ class RootNavViewModel @Inject constructor(
|
|||
val userState = action.userState
|
||||
val updatedRootNavState = when {
|
||||
userState == null || userState.hasPendingAccountAddition -> RootNavState.Auth
|
||||
userState.activeAccount.isVaultUnlocked -> RootNavState.VaultUnlocked
|
||||
userState.activeAccount.isVaultUnlocked -> {
|
||||
RootNavState.VaultUnlocked(
|
||||
activeUserId = userState.activeAccount.userId,
|
||||
)
|
||||
}
|
||||
|
||||
else -> RootNavState.VaultLocked
|
||||
}
|
||||
mutableStateFlow.update { updatedRootNavState }
|
||||
|
@ -72,10 +77,12 @@ sealed class RootNavState : Parcelable {
|
|||
data object VaultLocked : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show vault unlocked nav graph.
|
||||
* App should show vault unlocked nav graph for the given [activeUserId].
|
||||
*/
|
||||
@Parcelize
|
||||
data object VaultUnlocked : RootNavState()
|
||||
data class VaultUnlocked(
|
||||
val activeUserId: String,
|
||||
) : RootNavState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -146,7 +146,7 @@ class VaultViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLockAccountClick(action: VaultAction.LockAccountClick) {
|
||||
vaultRepository.lockVaultIfNecessary(userId = action.accountSummary.userId)
|
||||
vaultRepository.lockVault(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleLogoutAccountClick(action: VaultAction.LogoutAccountClick) {
|
||||
|
|
|
@ -103,6 +103,13 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
assertEquals(privateKey, storedPrivateKeys[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the [userAutoUnlockKey] was stored successfully using the [userId].
|
||||
*/
|
||||
fun assertUserAutoUnlockKey(userId: String, userAutoUnlockKey: String?) {
|
||||
assertEquals(userAutoUnlockKey, storedUserAutoUnlockKeys[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the the [organizationKeys] was stored successfully using the [userId].
|
||||
*/
|
||||
|
|
|
@ -7,6 +7,9 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
|||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
@ -22,23 +25,167 @@ import io.mockk.mockk
|
|||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultLockManagerTest {
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk {
|
||||
every { clearCrypto(userId = any()) } just runs
|
||||
}
|
||||
private val mutableVaultTimeoutStateFlow =
|
||||
MutableStateFlow<VaultTimeout>(VaultTimeout.ThirtyMinutes)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { getVaultTimeoutStateFlow(any()) } returns mutableVaultTimeoutStateFlow
|
||||
}
|
||||
|
||||
private val vaultLockManager: VaultLockManager = VaultLockManagerImpl(
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsRepository = settingsRepository,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `vaultTimeout updates to non-Never should clear the user's auto-unlock key`() = runTest {
|
||||
val userId = "mockId-1"
|
||||
val userAutoUnlockKey = "userAutoUnlockKey"
|
||||
|
||||
// Initialize Never state
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
} returns userAutoUnlockKey.asSuccess()
|
||||
verifyUnlockedVault(userId = userId)
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
|
||||
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = userAutoUnlockKey,
|
||||
)
|
||||
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
|
||||
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultTimeout update to Never for an unlocked account should store the user's encrypted key`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val userAutoUnlockKey = "userAutoUnlockKey"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
} returns userAutoUnlockKey.asSuccess()
|
||||
|
||||
verifyUnlockedVault(userId = userId)
|
||||
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
|
||||
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = userAutoUnlockKey,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultTimeout update to Never for a locked account when there is no stored private key should do nothing`() {
|
||||
val userId = "mockId-1"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = userId))
|
||||
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
|
||||
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = userId))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultTimeout update to Never for a locked account when there is no stored auto-unlock key should do nothing`() {
|
||||
val userId = "mockId-1"
|
||||
val privateKey = "privateKey"
|
||||
fakeAuthDiskSource.apply {
|
||||
userState = MOCK_USER_STATE
|
||||
storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = userId))
|
||||
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
|
||||
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = userId))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultTimeout update to Never for a locked account when there is a stored auto-unlock key should unlock the vault`() {
|
||||
val userId = "mockId-1"
|
||||
val privateKey = "privateKey"
|
||||
val userAutoUnlockKey = "userAutoUnlockKey"
|
||||
fakeAuthDiskSource.apply {
|
||||
userState = MOCK_USER_STATE
|
||||
storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
storeUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = userAutoUnlockKey,
|
||||
)
|
||||
}
|
||||
coEvery {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = MOCK_PROFILE.toSdkParams(),
|
||||
email = MOCK_PROFILE.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = userAutoUnlockKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = userId))
|
||||
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
|
||||
|
||||
assertTrue(vaultLockManager.isVaultUnlocked(userId = userId))
|
||||
|
||||
coVerify {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = MOCK_PROFILE.toSdkParams(),
|
||||
email = MOCK_PROFILE.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = userAutoUnlockKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isVaultUnlocked should return the correct value based on the vault lock state`() =
|
||||
runTest {
|
||||
|
|
|
@ -29,7 +29,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
every { logout(any()) } just runs
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk(relaxed = true) {
|
||||
every { lockVaultIfNecessary(any()) } just runs
|
||||
every { lockVault(any()) } just runs
|
||||
}
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
|
@ -98,7 +98,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `LockAccountClick should call lockVaultIfNecessary for the given account`() {
|
||||
fun `LockAccountClick should call lockVault for the given account`() {
|
||||
val accountUserId = "userId"
|
||||
val accountSummary = mockk<AccountSummary> {
|
||||
every { userId } returns accountUserId
|
||||
|
@ -107,7 +107,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.trySendAction(LandingAction.LockAccountClick(accountSummary))
|
||||
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = accountUserId) }
|
||||
verify { vaultRepository.lockVault(userId = accountUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -50,7 +50,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
every { logout(any()) } just runs
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk(relaxed = true) {
|
||||
every { lockVaultIfNecessary(any()) } just runs
|
||||
every { lockVault(any()) } just runs
|
||||
}
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
|
@ -160,7 +160,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `LockAccountClick should call lockVaultIfNecessary for the given account`() {
|
||||
fun `LockAccountClick should call lockVault for the given account`() {
|
||||
val accountUserId = "userId"
|
||||
val accountSummary = mockk<AccountSummary> {
|
||||
every { userId } returns accountUserId
|
||||
|
@ -169,7 +169,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.trySendAction(LoginAction.LockAccountClick(accountSummary))
|
||||
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = accountUserId) }
|
||||
verify { vaultRepository.lockVault(userId = accountUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -40,7 +40,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk(relaxed = true) {
|
||||
every { lockVaultIfNecessary(any()) } just runs
|
||||
every { lockVault(any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -87,8 +87,9 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@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 unlocked account should not update the state`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
|
@ -111,6 +112,37 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserState updates with a non-null locked account 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+test@bitwarden.com",
|
||||
avatarColorHex = "#00aaaa",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isVaultUnlocked = false,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
avatarColorString = "#00aaaa",
|
||||
|
@ -124,7 +156,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
avatarColorHex = "#00aaaa",
|
||||
environmentLabel = "bitwarden.com",
|
||||
isActive = true,
|
||||
isVaultUnlocked = true,
|
||||
isVaultUnlocked = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -172,7 +204,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on LockAccountClick should call lockVaultIfNecessary for the given account`() {
|
||||
fun `on LockAccountClick should call lockVault for the given account`() {
|
||||
val accountUserId = "userId"
|
||||
val accountSummary = mockk<AccountSummary> {
|
||||
every { userId } returns accountUserId
|
||||
|
@ -181,7 +213,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.trySendAction(VaultUnlockAction.LockAccountClick(accountSummary))
|
||||
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = accountUserId) }
|
||||
verify { vaultRepository.lockVault(userId = accountUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -82,7 +82,7 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
// Make sure navigating to vault unlocked works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlocked
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlocked(activeUserId = "userId")
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "vault_unlocked_graph",
|
||||
|
|
|
@ -43,7 +43,10 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(RootNavState.VaultUnlocked, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -57,7 +57,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
|
||||
every { sync() } returns Unit
|
||||
every { lockVaultForCurrentUser() } just runs
|
||||
every { lockVaultIfNecessary(any()) } just runs
|
||||
every { lockVault(any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -174,7 +174,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on LockAccountClick should call lockVaultIfNecessary for the given account`() {
|
||||
fun `on LockAccountClick should call lockVault for the given account`() {
|
||||
val accountUserId = "userId"
|
||||
val accountSummary = mockk<AccountSummary> {
|
||||
every { userId } returns accountUserId
|
||||
|
@ -184,7 +184,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.trySendAction(VaultAction.LockAccountClick(accountSummary))
|
||||
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = accountUserId) }
|
||||
verify { vaultRepository.lockVault(userId = accountUserId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
|
Loading…
Add table
Reference in a new issue