BIT-746, BIT-1120: Implement session timeout functionality (#605)

This commit is contained in:
Brian Yencho 2024-01-14 17:30:44 -06:00 committed by Álison Fernandes
parent 6a208fee31
commit d6d179a27f
11 changed files with 631 additions and 217 deletions

View file

@ -184,22 +184,12 @@ class AuthRepositoryImpl constructor(
when (loginResponse) { when (loginResponse) {
is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey) is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey)
is Success -> { is Success -> {
activeUserId?.let { previousActiveUserId ->
vaultRepository.lockVaultIfNecessary(userId = previousActiveUserId)
}
val userStateJson = loginResponse.toUserState( val userStateJson = loginResponse.toUserState(
previousUserState = authDiskSource.userState, previousUserState = authDiskSource.userState,
environmentUrlData = environmentRepository environmentUrlData = environmentRepository
.environment .environment
.environmentUrlData, .environmentUrlData,
) )
// Check for existing organization keys for a soft-logout account.
// We can separately unlock the vault for organization data after receiving
// the sync response if this data is currently absent.
val organizationKeys =
authDiskSource.getOrganizationKeys(
userId = userStateJson.activeUserId,
)
vaultRepository.clearUnlockedData() vaultRepository.clearUnlockedData()
vaultRepository.unlockVault( vaultRepository.unlockVault(
userId = userStateJson.activeUserId, userId = userStateJson.activeUserId,
@ -208,7 +198,9 @@ class AuthRepositoryImpl constructor(
userKey = loginResponse.key, userKey = loginResponse.key,
privateKey = loginResponse.privateKey, privateKey = loginResponse.privateKey,
masterPassword = password, masterPassword = password,
organizationKeys = organizationKeys, // We can separately unlock the vault for organization data after
// receiving the sync response if this data is currently absent.
organizationKeys = null,
) )
authDiskSource.userState = userStateJson authDiskSource.userState = userStateJson
authDiskSource.storeUserKey( authDiskSource.storeUserKey(
@ -286,8 +278,7 @@ class AuthRepositoryImpl constructor(
// Switch to the new user // Switch to the new user
authDiskSource.userState = currentUserState.copy(activeUserId = userId) authDiskSource.userState = currentUserState.copy(activeUserId = userId)
// Lock and clear data for the previous user // Clear data for the previous user
vaultRepository.lockVaultIfNecessary(previousActiveUserId)
vaultRepository.clearUnlockedData() vaultRepository.clearUnlockedData()
// Clear any special circumstances // Clear any special circumstances

View file

@ -30,11 +30,6 @@ interface VaultLockManager {
*/ */
fun lockVault(userId: String) fun lockVault(userId: String)
/**
* Locks the vault for the user with the given [userId] only if necessary.
*/
fun lockVaultIfNecessary(userId: String)
/** /**
* Locks the vault for the current user if currently unlocked. * Locks the vault for the current user if currently unlocked.
*/ */

View file

@ -1,14 +1,19 @@
package com.x8bit.bitwarden.data.vault.manager package com.x8bit.bitwarden.data.vault.manager
import android.os.SystemClock
import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.Kdf import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
@ -27,25 +32,33 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val SECONDS_PER_MINUTE = 60
private const val MILLISECONDS_PER_SECOND = 1000
/** /**
* Primary implementation [VaultLockManager]. * Primary implementation [VaultLockManager].
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions", "LongParameterList")
class VaultLockManagerImpl( class VaultLockManagerImpl(
private val authDiskSource: AuthDiskSource, private val authDiskSource: AuthDiskSource,
private val vaultSdkSource: VaultSdkSource, private val vaultSdkSource: VaultSdkSource,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val appForegroundManager: AppForegroundManager,
private val userLogoutManager: UserLogoutManager,
private val dispatcherManager: DispatcherManager, private val dispatcherManager: DispatcherManager,
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
) : VaultLockManager { ) : VaultLockManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
private val userIds: Set<String> get() = authDiskSource.userState?.accounts?.keys.orEmpty()
private val mutableVaultStateStateFlow = private val mutableVaultStateStateFlow =
MutableStateFlow( MutableStateFlow(
@ -59,6 +72,8 @@ class VaultLockManagerImpl(
get() = mutableVaultStateStateFlow.asStateFlow() get() = mutableVaultStateStateFlow.asStateFlow()
init { init {
observeAppForegroundChanges()
observeUserSwitchingChanges()
observeVaultTimeoutChanges() observeVaultTimeoutChanges()
} }
@ -78,15 +93,6 @@ class VaultLockManagerImpl(
} }
} }
override fun lockVaultIfNecessary(userId: String) {
// 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)
}
override suspend fun unlockVault( override suspend fun unlockVault(
userId: String, userId: String,
email: String, email: String,
@ -200,6 +206,61 @@ class VaultLockManagerImpl(
} }
} }
private fun observeAppForegroundChanges() {
var isFirstForeground = true
appForegroundManager
.appForegroundStateFlow
.onEach { appForegroundState ->
when (appForegroundState) {
AppForegroundState.BACKGROUNDED -> {
activeUserId?.let { updateLastActiveTime(userId = it) }
}
AppForegroundState.FOREGROUNDED -> {
userIds.forEach { userId ->
// If first foreground, clear the elapsed values so the timeout action
// is always performed.
if (isFirstForeground) {
authDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
}
checkForVaultTimeout(
userId = userId,
isAppRestart = isFirstForeground,
)
}
isFirstForeground = false
}
}
}
.launchIn(unconfinedScope)
}
private fun observeUserSwitchingChanges() {
var lastActiveUserId: String? = null
authDiskSource
.userStateFlow
.mapNotNull { it?.activeUserId }
.distinctUntilChanged()
.onEach { activeUserId ->
val previousActiveUserId = lastActiveUserId
lastActiveUserId = activeUserId
if (previousActiveUserId != null &&
activeUserId != previousActiveUserId
) {
handleUserSwitch(
previousActiveUserId = previousActiveUserId,
currentActiveUserId = activeUserId,
)
}
}
.launchIn(unconfinedScope)
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun observeVaultTimeoutChanges() { private fun observeVaultTimeoutChanges() {
authDiskSource authDiskSource
@ -264,6 +325,88 @@ class VaultLockManagerImpl(
} }
} }
/**
* Handles any vault timeout actions that may need to be performed for the given
* [previousActiveUserId] and [currentActiveUserId] during an account switch.
*/
private fun handleUserSwitch(
previousActiveUserId: String,
currentActiveUserId: String,
) {
// Check if the user's timeout action should be performed as we switch away.
checkForVaultTimeout(userId = previousActiveUserId)
// Set the last active time for the previous user.
updateLastActiveTime(userId = previousActiveUserId)
// Check if the vault timeout action should be performed for the current user
checkForVaultTimeout(userId = currentActiveUserId)
// Set the last active time for the current user.
updateLastActiveTime(userId = currentActiveUserId)
}
/**
* Checks the current [VaultTimeout] for the given [userId]. If the given timeout value has
* been exceeded, the [VaultTimeoutAction] for the given user will be performed.
*/
@Suppress("ReturnCount")
private fun checkForVaultTimeout(
userId: String,
isAppRestart: Boolean = false,
) {
val currentTimeMillis = elapsedRealtimeMillisProvider()
val lastActiveTimeMillis =
authDiskSource
.getLastActiveTimeMillis(userId = userId)
?: 0
val vaultTimeout =
settingsRepository.getVaultTimeoutStateFlow(userId = userId).value
val vaultTimeoutAction =
settingsRepository.getVaultTimeoutActionStateFlow(userId = userId).value
val vaultTimeoutInMinutes = when (vaultTimeout) {
VaultTimeout.Never -> {
// No action to take for Never timeout.
return
}
VaultTimeout.OnAppRestart -> {
// If this is an app restart, trigger the timeout action; otherwise ignore.
if (isAppRestart) 0 else return
}
else -> vaultTimeout.vaultTimeoutInMinutes ?: return
}
val vaultTimeoutInMillis = vaultTimeoutInMinutes *
SECONDS_PER_MINUTE *
MILLISECONDS_PER_SECOND
if (currentTimeMillis - lastActiveTimeMillis >= vaultTimeoutInMillis) {
// Perform lock / logout!
when (vaultTimeoutAction) {
VaultTimeoutAction.LOCK -> {
setVaultToLocked(userId = userId)
}
VaultTimeoutAction.LOGOUT -> {
setVaultToLocked(userId = userId)
userLogoutManager.softLogout(userId = userId)
}
}
}
}
/**
* Sets the "last active time" for the given [userId] to the current time.
*/
private fun updateLastActiveTime(userId: String) {
val elapsedRealtimeMillis = elapsedRealtimeMillisProvider()
authDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = elapsedRealtimeMillis,
)
}
@Suppress("ReturnCount") @Suppress("ReturnCount")
private suspend fun unlockVaultForUser( private suspend fun unlockVaultForUser(
userId: String, userId: String,

View file

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.data.vault.manager.di package com.x8bit.bitwarden.data.vault.manager.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
@ -25,12 +27,16 @@ object VaultManagerModule {
authDiskSource: AuthDiskSource, authDiskSource: AuthDiskSource,
vaultSdkSource: VaultSdkSource, vaultSdkSource: VaultSdkSource,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
appForegroundManager: AppForegroundManager,
userLogoutManager: UserLogoutManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
): VaultLockManager = ): VaultLockManager =
VaultLockManagerImpl( VaultLockManagerImpl(
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
appForegroundManager = appForegroundManager,
userLogoutManager = userLogoutManager,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
) )
} }

View file

@ -159,9 +159,6 @@ class AccountSecurityViewModel @Inject constructor(
) )
} }
settingsRepository.vaultTimeout = vaultTimeout settingsRepository.vaultTimeout = vaultTimeout
// TODO: Finish implementing vault timeouts (BIT-1120)
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
} }
private fun handleVaultTimeoutActionSelect( private fun handleVaultTimeoutActionSelect(
@ -174,9 +171,6 @@ class AccountSecurityViewModel @Inject constructor(
) )
} }
settingsRepository.vaultTimeoutAction = vaultTimeoutAction settingsRepository.vaultTimeoutAction = vaultTimeoutAction
// TODO BIT-746: Finish implementing session timeout action
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
} }
private fun handleTwoStepLoginClick() { private fun handleTwoStepLoginClick() {

View file

@ -129,6 +129,13 @@ class FakeAuthDiskSource : AuthDiskSource {
assertEquals(userState, this.userState) assertEquals(userState, this.userState)
} }
/**
* Assert that the [lastActiveTimeMillis] was stored successfully using the [userId].
*/
fun assertLastActiveTimeMillis(userId: String, lastActiveTimeMillis: Long?) {
assertEquals(lastActiveTimeMillis, storedLastActiveTimeMillis[userId])
}
/** /**
* Assert that the [userKey] was stored successfully using the [userId]. * Assert that the [userKey] was stored successfully using the [userId].
*/ */

View file

@ -82,7 +82,6 @@ class AuthRepositoryTest {
private val vaultRepository: VaultRepository = mockk { private val vaultRepository: VaultRepository = mockk {
every { vaultStateFlow } returns mutableVaultStateFlow every { vaultStateFlow } returns mutableVaultStateFlow
every { deleteVaultData(any()) } just runs every { deleteVaultData(any()) } just runs
every { lockVaultIfNecessary(any()) } just runs
every { clearUnlockedData() } just runs every { clearUnlockedData() } just runs
} }
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
@ -559,7 +558,6 @@ class AuthRepositoryTest {
) )
assertNull(repository.specialCircumstance) assertNull(repository.specialCircumstance)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
verify { vaultRepository.clearUnlockedData() } verify { vaultRepository.clearUnlockedData() }
} }
@ -641,90 +639,6 @@ class AuthRepositoryTest {
) )
assertNull(repository.specialCircumstance) assertNull(repository.specialCircumstance)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_2) }
verify { vaultRepository.clearUnlockedData() }
}
@Suppress("MaxLineLength")
@Test
fun `login get token succeeds when the current user is in a soft-logout state should use existing organization keys when unlocking the vault`() =
runTest {
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
accountsService.preLogin(email = EMAIL)
} returns Result.success(PRE_LOGIN_SUCCESS)
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
.returns(Result.success(successResponse))
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationKeys = ORGANIZATION_KEYS,
masterPassword = PASSWORD,
)
} returns VaultUnlockResult.Success
coEvery { vaultRepository.sync() } just runs
every {
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
previousUserState = null,
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
)
} returns SINGLE_USER_STATE_1
// Users in a soft-logout state have some existing data stored to disk from previous
// sync requests.
fakeAuthDiskSource.storeOrganizationKeys(
userId = USER_ID_1,
organizationKeys = ORGANIZATION_KEYS,
)
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
assertEquals(LoginResult.Success, result)
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
coVerify { accountsService.preLogin(email = EMAIL) }
fakeAuthDiskSource.assertPrivateKey(
userId = USER_ID_1,
privateKey = "privateKey",
)
fakeAuthDiskSource.assertUserKey(
userId = USER_ID_1,
userKey = "key",
)
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationKeys = ORGANIZATION_KEYS,
masterPassword = PASSWORD,
)
vaultRepository.sync()
}
assertEquals(
SINGLE_USER_STATE_1,
fakeAuthDiskSource.userState,
)
assertNull(repository.specialCircumstance)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
verify { vaultRepository.clearUnlockedData() } verify { vaultRepository.clearUnlockedData() }
} }
@ -1129,7 +1043,6 @@ class AuthRepositoryTest {
) )
assertNull(repository.userStateFlow.value) assertNull(repository.userStateFlow.value)
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
verify(exactly = 0) { vaultRepository.clearUnlockedData() } verify(exactly = 0) { vaultRepository.clearUnlockedData() }
} }
@ -1159,7 +1072,6 @@ class AuthRepositoryTest {
repository.userStateFlow.value, repository.userStateFlow.value,
) )
assertNull(repository.specialCircumstance) assertNull(repository.specialCircumstance)
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) }
verify(exactly = 0) { vaultRepository.clearUnlockedData() } verify(exactly = 0) { vaultRepository.clearUnlockedData() }
} }
@ -1188,13 +1100,12 @@ class AuthRepositoryTest {
originalUserState, originalUserState,
repository.userStateFlow.value, repository.userStateFlow.value,
) )
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) }
verify(exactly = 0) { vaultRepository.clearUnlockedData() } verify(exactly = 0) { vaultRepository.clearUnlockedData() }
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `switchAccount when the userId is valid should update the current UserState, lock the vault of the previous active user, clear the previously unlocked data, and reset the special circumstance`() { fun `switchAccount when the userId is valid should update the current UserState, clear the previously unlocked data, and reset the special circumstance`() {
val originalUserId = USER_ID_1 val originalUserId = USER_ID_1
val updatedUserId = USER_ID_2 val updatedUserId = USER_ID_2
val originalUserState = MULTI_USER_STATE.toUserState( val originalUserState = MULTI_USER_STATE.toUserState(
@ -1219,7 +1130,6 @@ class AuthRepositoryTest {
repository.userStateFlow.value, repository.userStateFlow.value,
) )
assertNull(repository.specialCircumstance) assertNull(repository.specialCircumstance)
verify { vaultRepository.lockVaultIfNecessary(originalUserId) }
verify { vaultRepository.clearUnlockedData() } verify { vaultRepository.clearUnlockedData() }
} }

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.platform.manager.util
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* A faked implementation of [AppForegroundManager]
*/
class FakeAppForegroundManager : AppForegroundManager {
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
override val appForegroundStateFlow: StateFlow<AppForegroundState>
get() = mutableAppForegroundStateFlow.asStateFlow()
/**
* The current [AppForegroundState] tracked by the [appForegroundStateFlow].
*/
var appForegroundState: AppForegroundState
get() = mutableAppForegroundStateFlow.value
set(value) {
mutableAppForegroundStateFlow.value = value
}
}

View file

@ -6,10 +6,14 @@ import com.bitwarden.core.InitUserCryptoRequest
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson 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.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.manager.util.FakeAppForegroundManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
@ -17,6 +21,7 @@ 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.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import io.mockk.awaits import io.mockk.awaits
import io.mockk.clearMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
@ -26,6 +31,7 @@ import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
@ -35,22 +41,396 @@ import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class VaultLockManagerTest { class VaultLockManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeAppForegroundManager = FakeAppForegroundManager()
private val vaultSdkSource: VaultSdkSource = mockk { private val vaultSdkSource: VaultSdkSource = mockk {
every { clearCrypto(userId = any()) } just runs every { clearCrypto(userId = any()) } just runs
} }
private val userLogoutManager: UserLogoutManager = mockk {
every { logout(any()) } just runs
every { softLogout(any()) } just runs
}
private val mutableVaultTimeoutStateFlow = private val mutableVaultTimeoutStateFlow =
MutableStateFlow<VaultTimeout>(VaultTimeout.ThirtyMinutes) MutableStateFlow<VaultTimeout>(VaultTimeout.ThirtyMinutes)
private val mutableVaultTimeoutActionStateFlow =
MutableStateFlow<VaultTimeoutAction>(VaultTimeoutAction.LOCK)
private val settingsRepository: SettingsRepository = mockk { private val settingsRepository: SettingsRepository = mockk {
every { getVaultTimeoutStateFlow(any()) } returns mutableVaultTimeoutStateFlow every { getVaultTimeoutStateFlow(any()) } returns mutableVaultTimeoutStateFlow
every { getVaultTimeoutActionStateFlow(any()) } returns mutableVaultTimeoutActionStateFlow
} }
private var elapsedRealtimeMillis = 123456789L
private val vaultLockManager: VaultLockManager = VaultLockManagerImpl( private val vaultLockManager: VaultLockManager = VaultLockManagerImpl(
authDiskSource = fakeAuthDiskSource, authDiskSource = fakeAuthDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
appForegroundManager = fakeAppForegroundManager,
userLogoutManager = userLogoutManager,
dispatcherManager = FakeDispatcherManager(), dispatcherManager = FakeDispatcherManager(),
elapsedRealtimeMillisProvider = { elapsedRealtimeMillis },
) )
@Test
fun `app going into background should update the current user's last active time`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Start in a foregrounded state
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
elapsedRealtimeMillis = 123L
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = 123L,
)
}
@Suppress("MaxLineLength")
@Test
fun `app coming into foreground for the first time for Never timeout should clear existing times and not perform timeout action`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = 123L,
)
verifyUnlockedVaultBlocking(userId = userId)
assertTrue(vaultLockManager.isVaultUnlocked(userId))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertTrue(vaultLockManager.isVaultUnlocked(userId))
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
}
@Suppress("MaxLineLength")
@Test
fun `app coming into foreground for the first time for OnAppRestart timeout should clear existing times and lock vaults if necessary`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.OnAppRestart
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = 123L,
)
verifyUnlockedVaultBlocking(userId = userId)
assertTrue(vaultLockManager.isVaultUnlocked(userId))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(userId))
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
}
@Suppress("MaxLineLength")
@Test
fun `app coming into foreground for the first time for other timeout should clear existing times and lock vaults if necessary`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = 123L,
)
verifyUnlockedVaultBlocking(userId = userId)
assertTrue(vaultLockManager.isVaultUnlocked(userId))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(userId))
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
}
@Suppress("MaxLineLength")
@Test
fun `app coming into foreground for the first time for non-Never timeout should clear existing times and perform timeout action`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = 123L,
)
verifyUnlockedVaultBlocking(userId = userId)
assertTrue(vaultLockManager.isVaultUnlocked(userId))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(userId))
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
}
@Suppress("MaxLineLength")
@Test
fun `app coming into foreground subsequent times should perform timeout action if necessary and not clear existing times`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Start in a foregrounded state
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = null,
)
// Set the last active time to 2 minutes and the current time to 8 minutes, so only times
// beyond 6 minutes perform their action.
val lastActiveTime = 2 * 60 * 1000L
elapsedRealtimeMillis = 8 * 60 * 1000L
// Will be used within each loop to reset the test to a suitable initial state.
fun resetTest(vaultTimeout: VaultTimeout) {
clearVerifications(userLogoutManager)
mutableVaultTimeoutStateFlow.value = vaultTimeout
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = lastActiveTime,
)
verifyUnlockedVaultBlocking(userId = userId)
assertTrue(vaultLockManager.isVaultUnlocked(userId))
}
// Test Lock action
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
when (vaultTimeout) {
// After 6 minutes (or action should not be performed)
VaultTimeout.Never,
VaultTimeout.OnAppRestart,
VaultTimeout.FifteenMinutes,
VaultTimeout.ThirtyMinutes,
VaultTimeout.OneHour,
VaultTimeout.FourHours,
is VaultTimeout.Custom,
-> {
assertTrue(vaultLockManager.isVaultUnlocked(userId))
}
// Before 6 minutes
VaultTimeout.Immediately,
VaultTimeout.OneMinute,
VaultTimeout.FiveMinutes,
-> {
assertFalse(vaultLockManager.isVaultUnlocked(userId))
}
}
verify(exactly = 0) { userLogoutManager.softLogout(any()) }
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = lastActiveTime,
)
}
// Test Logout action
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
when (vaultTimeout) {
// After 6 minutes (or action should not be performed)
VaultTimeout.Never,
VaultTimeout.OnAppRestart,
VaultTimeout.FifteenMinutes,
VaultTimeout.ThirtyMinutes,
VaultTimeout.OneHour,
VaultTimeout.FourHours,
is VaultTimeout.Custom,
-> {
assertTrue(vaultLockManager.isVaultUnlocked(userId))
verify(exactly = 0) { userLogoutManager.softLogout(any()) }
}
// Before 6 minutes
VaultTimeout.Immediately,
VaultTimeout.OneMinute,
VaultTimeout.FiveMinutes,
-> {
assertFalse(vaultLockManager.isVaultUnlocked(userId))
verify(exactly = 1) { userLogoutManager.softLogout(userId) }
}
}
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = lastActiveTime,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `switching users should perform lock actions for each user if necessary and reset their last active times`() {
val userId1 = "mockId-1"
val userId2 = "mockId-2"
fakeAuthDiskSource.userState = UserStateJson(
activeUserId = userId1,
accounts = mapOf(
userId1 to MOCK_ACCOUNT,
userId2 to MOCK_ACCOUNT.copy(profile = MOCK_PROFILE.copy(userId = userId2)),
),
)
// Set the last active time to 2 minutes and the current time to 8 minutes, so only times
// beyond 6 minutes perform their action.
val lastActiveTime = 2 * 60 * 1000L
elapsedRealtimeMillis = 8 * 60 * 1000L
// Will be used within each loop to reset the test to a suitable initial state.
fun resetTest(vaultTimeout: VaultTimeout) {
clearVerifications(userLogoutManager)
mutableVaultTimeoutStateFlow.value = vaultTimeout
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId1,
lastActiveTimeMillis = lastActiveTime,
)
fakeAuthDiskSource.storeLastActiveTimeMillis(
userId = userId2,
lastActiveTimeMillis = lastActiveTime,
)
verifyUnlockedVaultBlocking(userId = userId1)
verifyUnlockedVaultBlocking(userId = userId2)
assertTrue(vaultLockManager.isVaultUnlocked(userId1))
assertTrue(vaultLockManager.isVaultUnlocked(userId2))
}
// Test Lock action
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout)
fakeAuthDiskSource.userState = fakeAuthDiskSource.userState?.copy(
activeUserId = if (fakeAuthDiskSource.userState?.activeUserId == userId1) {
userId2
} else {
userId1
},
)
when (vaultTimeout) {
// After 6 minutes (or action should not be performed)
VaultTimeout.Never,
VaultTimeout.OnAppRestart,
VaultTimeout.FifteenMinutes,
VaultTimeout.ThirtyMinutes,
VaultTimeout.OneHour,
VaultTimeout.FourHours,
is VaultTimeout.Custom,
-> {
assertTrue(vaultLockManager.isVaultUnlocked(userId1))
assertTrue(vaultLockManager.isVaultUnlocked(userId2))
}
// Before 6 minutes
VaultTimeout.Immediately,
VaultTimeout.OneMinute,
VaultTimeout.FiveMinutes,
-> {
assertFalse(vaultLockManager.isVaultUnlocked(userId1))
assertFalse(vaultLockManager.isVaultUnlocked(userId2))
}
}
verify(exactly = 0) { userLogoutManager.softLogout(any()) }
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId1,
lastActiveTimeMillis = elapsedRealtimeMillis,
)
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId2,
lastActiveTimeMillis = elapsedRealtimeMillis,
)
}
// Test Logout action
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout)
fakeAuthDiskSource.userState = fakeAuthDiskSource.userState?.copy(
activeUserId = if (fakeAuthDiskSource.userState?.activeUserId == userId1) {
userId2
} else {
userId1
},
)
when (vaultTimeout) {
// After 6 minutes (or action should not be performed)
VaultTimeout.Never,
VaultTimeout.OnAppRestart,
VaultTimeout.FifteenMinutes,
VaultTimeout.ThirtyMinutes,
VaultTimeout.OneHour,
VaultTimeout.FourHours,
is VaultTimeout.Custom,
-> {
assertTrue(vaultLockManager.isVaultUnlocked(userId1))
assertTrue(vaultLockManager.isVaultUnlocked(userId2))
verify(exactly = 0) { userLogoutManager.softLogout(any()) }
}
// Before 6 minutes
VaultTimeout.Immediately,
VaultTimeout.OneMinute,
VaultTimeout.FiveMinutes,
-> {
assertFalse(vaultLockManager.isVaultUnlocked(userId1))
assertFalse(vaultLockManager.isVaultUnlocked(userId2))
verify(exactly = 1) { userLogoutManager.softLogout(userId1) }
verify(exactly = 1) { userLogoutManager.softLogout(userId2) }
}
}
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId1,
lastActiveTimeMillis = elapsedRealtimeMillis,
)
fakeAuthDiskSource.assertLastActiveTimeMillis(
userId = userId2,
lastActiveTimeMillis = elapsedRealtimeMillis,
)
}
}
@Test @Test
fun `vaultTimeout updates to non-Never should clear the user's auto-unlock key`() = runTest { fun `vaultTimeout updates to non-Never should clear the user's auto-unlock key`() = runTest {
val userId = "mockId-1" val userId = "mockId-1"
@ -271,59 +651,6 @@ class VaultLockManagerTest {
verify { vaultSdkSource.clearCrypto(userId = userId) } verify { vaultSdkSource.clearCrypto(userId = userId) }
} }
@Suppress("MaxLineLength")
@Test
fun `lockVaultIfNecessary when non-Never timeout should lock the given account if it is currently unlocked`() =
runTest {
val userId = "userId"
verifyUnlockedVault(userId = userId)
assertEquals(
VaultState(
unlockedVaultUserIds = setOf(userId),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
vaultLockManager.lockVaultIfNecessary(userId = userId)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
verify { vaultSdkSource.clearCrypto(userId = userId) }
}
@Suppress("MaxLineLength")
@Test
fun `lockVaultIfNecessary when Never timeout should not lock the given account if it is currently unlocked`() =
runTest {
val userId = "userId"
verifyUnlockedVault(userId = userId)
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
val initialVaultState = VaultState(
unlockedVaultUserIds = setOf(userId),
unlockingVaultUserIds = emptySet(),
)
assertEquals(
initialVaultState,
vaultLockManager.vaultStateFlow.value,
)
vaultLockManager.lockVaultIfNecessary(userId = userId)
assertEquals(
initialVaultState,
vaultLockManager.vaultStateFlow.value,
)
verify(exactly = 0) { vaultSdkSource.clearCrypto(userId = userId) }
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `lockVaultForCurrentUser should lock the vault for the current user if it is currently unlocked`() = fun `lockVaultForCurrentUser should lock the vault for the current user if it is currently unlocked`() =
@ -840,6 +1167,21 @@ class VaultLockManagerTest {
} }
} }
/**
* Resets the verification call count for the given [mock] while leaving all other mocked
* behavior in place.
*/
private fun clearVerifications(mock: Any) {
clearMocks(
firstMock = mock,
recordedCalls = true,
answers = false,
childMocks = false,
verificationMarks = false,
exclusionRules = false,
)
}
/** /**
* Helper to ensures that the vault for the user with the given [userId] is actively unlocking. * Helper to ensures that the vault for the user with the given [userId] is actively unlocking.
* Note that this call will actively hang. * Note that this call will actively hang.
@ -889,6 +1231,10 @@ class VaultLockManagerTest {
val userKey = "12345" val userKey = "12345"
val privateKey = "54321" val privateKey = "54321"
val organizationKeys = null val organizationKeys = null
val userAutoUnlockKey = "userAutoUnlockKey"
// Clear recorded calls so this helper can be called multiple times and assert a unique
// unlock has happened each time.
clearVerifications(vaultSdkSource)
coEvery { coEvery {
vaultSdkSource.initializeCrypto( vaultSdkSource.initializeCrypto(
userId = userId, userId = userId,
@ -903,6 +1249,9 @@ class VaultLockManagerTest {
), ),
) )
} returns InitializeCryptoResult.Success.asSuccess() } returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.getUserEncryptionKey(userId = userId)
} returns userAutoUnlockKey.asSuccess()
val result = vaultLockManager.unlockVault( val result = vaultLockManager.unlockVault(
userId = userId, userId = userId,
@ -932,6 +1281,25 @@ class VaultLockManagerTest {
) )
} }
} }
private fun verifyUnlockedVaultBlocking(userId: String) {
runBlocking { verifyUnlockedVault(userId = userId) }
}
}
private val MOCK_TIMEOUTS = VaultTimeout.Type.entries.map {
when (it) {
VaultTimeout.Type.IMMEDIATELY -> VaultTimeout.Immediately
VaultTimeout.Type.ONE_MINUTE -> VaultTimeout.OneMinute
VaultTimeout.Type.FIVE_MINUTES -> VaultTimeout.FiveMinutes
VaultTimeout.Type.FIFTEEN_MINUTES -> VaultTimeout.FifteenMinutes
VaultTimeout.Type.THIRTY_MINUTES -> VaultTimeout.ThirtyMinutes
VaultTimeout.Type.ONE_HOUR -> VaultTimeout.OneHour
VaultTimeout.Type.FOUR_HOURS -> VaultTimeout.FourHours
VaultTimeout.Type.ON_APP_RESTART -> VaultTimeout.OnAppRestart
VaultTimeout.Type.NEVER -> VaultTimeout.Never
VaultTimeout.Type.CUSTOM -> VaultTimeout.Custom(vaultTimeoutInMinutes = 123)
}
} }
private val MOCK_PROFILE = AccountJson.Profile( private val MOCK_PROFILE = AccountJson.Profile(

View file

@ -100,7 +100,6 @@ class VaultRepositoryTest {
every { isVaultUnlocked(any()) } returns false every { isVaultUnlocked(any()) } returns false
every { isVaultUnlocking(any()) } returns false every { isVaultUnlocking(any()) } returns false
every { lockVault(any()) } just runs every { lockVault(any()) } just runs
every { lockVaultIfNecessary(any()) } just runs
every { lockVaultForCurrentUser() } just runs every { lockVaultForCurrentUser() } just runs
} }
@ -552,13 +551,6 @@ class VaultRepositoryTest {
verify { vaultLockManager.lockVaultForCurrentUser() } verify { vaultLockManager.lockVaultForCurrentUser() }
} }
@Test
fun `lockVaultIfNecessary should delete to the VaultLockManager`() {
val userId = "userId"
vaultRepository.lockVaultIfNecessary(userId = userId)
verify { vaultLockManager.lockVaultIfNecessary(userId = userId) }
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `unlockVaultAndSyncForCurrentUser with VaultLockManager Success should unlock for the current user, sync, and return Success`() = fun `unlockVaultAndSyncForCurrentUser with VaultLockManager Success should unlock for the current user, sync, and return Success`() =

View file

@ -123,20 +123,14 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `on VaultTimeoutTypeSelect should update the selection and emit ShowToast()`() = runTest { fun `on VaultTimeoutTypeSelect should update the selection()`() = runTest {
val settingsRepository = mockk<SettingsRepository>() { val settingsRepository = mockk<SettingsRepository>() {
every { vaultTimeout = any() } just runs every { vaultTimeout = any() } just runs
} }
val viewModel = createViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.trySendAction(
viewModel.trySendAction( AccountSecurityAction.VaultTimeoutTypeSelect(VaultTimeout.Type.FOUR_HOURS),
AccountSecurityAction.VaultTimeoutTypeSelect(VaultTimeout.Type.FOUR_HOURS), )
)
assertEquals(
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
awaitItem(),
)
}
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
vaultTimeout = VaultTimeout.FourHours, vaultTimeout = VaultTimeout.FourHours,
@ -147,22 +141,16 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `on CustomVaultTimeoutSelect should update the selection and emit ShowToast()`() = runTest { fun `on CustomVaultTimeoutSelect should update the selection()`() = runTest {
val settingsRepository = mockk<SettingsRepository>() { val settingsRepository = mockk<SettingsRepository>() {
every { vaultTimeout = any() } just runs every { vaultTimeout = any() } just runs
} }
val viewModel = createViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.trySendAction(
viewModel.trySendAction( AccountSecurityAction.CustomVaultTimeoutSelect(
AccountSecurityAction.CustomVaultTimeoutSelect( customVaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 360),
customVaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 360), ),
), )
)
assertEquals(
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
awaitItem(),
)
}
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 360), vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 360),
@ -180,15 +168,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
every { vaultTimeoutAction = any() } just runs every { vaultTimeoutAction = any() } just runs
} }
val viewModel = createViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test { viewModel.trySendAction(
viewModel.trySendAction( AccountSecurityAction.VaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT),
AccountSecurityAction.VaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT), )
)
assertEquals(
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
awaitItem(),
)
}
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
vaultTimeoutAction = VaultTimeoutAction.LOGOUT, vaultTimeoutAction = VaultTimeoutAction.LOGOUT,