mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-746, BIT-1120: Implement session timeout functionality (#605)
This commit is contained in:
parent
6a208fee31
commit
d6d179a27f
11 changed files with 631 additions and 217 deletions
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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`() =
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue