BIT-1019, BIT-1190: Add "Never Lock" vault timeout implementation (#559)

This commit is contained in:
Brian Yencho 2024-01-10 09:45:11 -06:00 committed by Álison Fernandes
parent 38cd8984e9
commit 125c304d12
16 changed files with 350 additions and 28 deletions

View file

@ -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,
)
}
}

View file

@ -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,
)
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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()
}
/**

View file

@ -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) {

View file

@ -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].
*/

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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")