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) {
|
||||
is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey)
|
||||
is Success -> {
|
||||
activeUserId?.let { previousActiveUserId ->
|
||||
vaultRepository.lockVaultIfNecessary(userId = previousActiveUserId)
|
||||
}
|
||||
val userStateJson = loginResponse.toUserState(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository
|
||||
.environment
|
||||
.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.unlockVault(
|
||||
userId = userStateJson.activeUserId,
|
||||
|
@ -208,7 +198,9 @@ class AuthRepositoryImpl constructor(
|
|||
userKey = loginResponse.key,
|
||||
privateKey = loginResponse.privateKey,
|
||||
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.storeUserKey(
|
||||
|
@ -286,8 +278,7 @@ class AuthRepositoryImpl constructor(
|
|||
// Switch to the new user
|
||||
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
||||
|
||||
// Lock and clear data for the previous user
|
||||
vaultRepository.lockVaultIfNecessary(previousActiveUserId)
|
||||
// Clear data for the previous user
|
||||
vaultRepository.clearUnlockedData()
|
||||
|
||||
// Clear any special circumstances
|
||||
|
|
|
@ -30,11 +30,6 @@ interface VaultLockManager {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
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.model.AppForegroundState
|
||||
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.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
@ -27,25 +32,33 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val SECONDS_PER_MINUTE = 60
|
||||
private const val MILLISECONDS_PER_SECOND = 1000
|
||||
|
||||
/**
|
||||
* Primary implementation [VaultLockManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
class VaultLockManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val appForegroundManager: AppForegroundManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
|
||||
) : VaultLockManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
private val userIds: Set<String> get() = authDiskSource.userState?.accounts?.keys.orEmpty()
|
||||
|
||||
private val mutableVaultStateStateFlow =
|
||||
MutableStateFlow(
|
||||
|
@ -59,6 +72,8 @@ class VaultLockManagerImpl(
|
|||
get() = mutableVaultStateStateFlow.asStateFlow()
|
||||
|
||||
init {
|
||||
observeAppForegroundChanges()
|
||||
observeUserSwitchingChanges()
|
||||
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(
|
||||
userId: 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)
|
||||
private fun observeVaultTimeoutChanges() {
|
||||
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")
|
||||
private suspend fun unlockVaultForUser(
|
||||
userId: String,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.vault.manager.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.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.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
@ -25,12 +27,16 @@ object VaultManagerModule {
|
|||
authDiskSource: AuthDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
settingsRepository: SettingsRepository,
|
||||
appForegroundManager: AppForegroundManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): VaultLockManager =
|
||||
VaultLockManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsRepository = settingsRepository,
|
||||
appForegroundManager = appForegroundManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -159,9 +159,6 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
settingsRepository.vaultTimeout = vaultTimeout
|
||||
|
||||
// TODO: Finish implementing vault timeouts (BIT-1120)
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleVaultTimeoutActionSelect(
|
||||
|
@ -174,9 +171,6 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
settingsRepository.vaultTimeoutAction = vaultTimeoutAction
|
||||
|
||||
// TODO BIT-746: Finish implementing session timeout action
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleTwoStepLoginClick() {
|
||||
|
|
|
@ -129,6 +129,13 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
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].
|
||||
*/
|
||||
|
|
|
@ -82,7 +82,6 @@ class AuthRepositoryTest {
|
|||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { vaultStateFlow } returns mutableVaultStateFlow
|
||||
every { deleteVaultData(any()) } just runs
|
||||
every { lockVaultIfNecessary(any()) } just runs
|
||||
every { clearUnlockedData() } just runs
|
||||
}
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
|
@ -559,7 +558,6 @@ class AuthRepositoryTest {
|
|||
)
|
||||
assertNull(repository.specialCircumstance)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
|
@ -641,90 +639,6 @@ class AuthRepositoryTest {
|
|||
)
|
||||
assertNull(repository.specialCircumstance)
|
||||
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() }
|
||||
}
|
||||
|
||||
|
@ -1129,7 +1043,6 @@ class AuthRepositoryTest {
|
|||
)
|
||||
|
||||
assertNull(repository.userStateFlow.value)
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
|
@ -1159,7 +1072,6 @@ class AuthRepositoryTest {
|
|||
repository.userStateFlow.value,
|
||||
)
|
||||
assertNull(repository.specialCircumstance)
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
|
@ -1188,13 +1100,12 @@ class AuthRepositoryTest {
|
|||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(originalUserId) }
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `switchAccount when the userId is valid should update the current UserState, lock the vault of the previous active user, 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 updatedUserId = USER_ID_2
|
||||
val originalUserState = MULTI_USER_STATE.toUserState(
|
||||
|
@ -1219,7 +1130,6 @@ class AuthRepositoryTest {
|
|||
repository.userStateFlow.value,
|
||||
)
|
||||
assertNull(repository.specialCircumstance)
|
||||
verify { vaultRepository.lockVaultIfNecessary(originalUserId) }
|
||||
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.UserStateJson
|
||||
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.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.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.asSuccess
|
||||
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.VaultUnlockResult
|
||||
import io.mockk.awaits
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
|
@ -26,6 +31,7 @@ import io.mockk.runs
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
|
@ -35,22 +41,396 @@ import org.junit.jupiter.api.Test
|
|||
@Suppress("LargeClass")
|
||||
class VaultLockManagerTest {
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val fakeAppForegroundManager = FakeAppForegroundManager()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk {
|
||||
every { clearCrypto(userId = any()) } just runs
|
||||
}
|
||||
private val userLogoutManager: UserLogoutManager = mockk {
|
||||
every { logout(any()) } just runs
|
||||
every { softLogout(any()) } just runs
|
||||
}
|
||||
private val mutableVaultTimeoutStateFlow =
|
||||
MutableStateFlow<VaultTimeout>(VaultTimeout.ThirtyMinutes)
|
||||
private val mutableVaultTimeoutActionStateFlow =
|
||||
MutableStateFlow<VaultTimeoutAction>(VaultTimeoutAction.LOCK)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { getVaultTimeoutStateFlow(any()) } returns mutableVaultTimeoutStateFlow
|
||||
every { getVaultTimeoutActionStateFlow(any()) } returns mutableVaultTimeoutActionStateFlow
|
||||
}
|
||||
|
||||
private var elapsedRealtimeMillis = 123456789L
|
||||
|
||||
private val vaultLockManager: VaultLockManager = VaultLockManagerImpl(
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsRepository = settingsRepository,
|
||||
appForegroundManager = fakeAppForegroundManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
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
|
||||
fun `vaultTimeout updates to non-Never should clear the user's auto-unlock key`() = runTest {
|
||||
val userId = "mockId-1"
|
||||
|
@ -271,59 +651,6 @@ class VaultLockManagerTest {
|
|||
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")
|
||||
@Test
|
||||
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.
|
||||
* Note that this call will actively hang.
|
||||
|
@ -889,6 +1231,10 @@ class VaultLockManagerTest {
|
|||
val userKey = "12345"
|
||||
val privateKey = "54321"
|
||||
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 {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
|
@ -903,6 +1249,9 @@ class VaultLockManagerTest {
|
|||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
} returns userAutoUnlockKey.asSuccess()
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
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(
|
||||
|
|
|
@ -100,7 +100,6 @@ class VaultRepositoryTest {
|
|||
every { isVaultUnlocked(any()) } returns false
|
||||
every { isVaultUnlocking(any()) } returns false
|
||||
every { lockVault(any()) } just runs
|
||||
every { lockVaultIfNecessary(any()) } just runs
|
||||
every { lockVaultForCurrentUser() } just runs
|
||||
}
|
||||
|
||||
|
@ -552,13 +551,6 @@ class VaultRepositoryTest {
|
|||
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")
|
||||
@Test
|
||||
fun `unlockVaultAndSyncForCurrentUser with VaultLockManager Success should unlock for the current user, sync, and return Success`() =
|
||||
|
|
|
@ -123,20 +123,14 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on VaultTimeoutTypeSelect should update the selection and emit ShowToast()`() = runTest {
|
||||
fun `on VaultTimeoutTypeSelect should update the selection()`() = runTest {
|
||||
val settingsRepository = mockk<SettingsRepository>() {
|
||||
every { vaultTimeout = any() } just runs
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.VaultTimeoutTypeSelect(VaultTimeout.Type.FOUR_HOURS),
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
vaultTimeout = VaultTimeout.FourHours,
|
||||
|
@ -147,22 +141,16 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on CustomVaultTimeoutSelect should update the selection and emit ShowToast()`() = runTest {
|
||||
fun `on CustomVaultTimeoutSelect should update the selection()`() = runTest {
|
||||
val settingsRepository = mockk<SettingsRepository>() {
|
||||
every { vaultTimeout = any() } just runs
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.CustomVaultTimeoutSelect(
|
||||
customVaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 360),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 360),
|
||||
|
@ -180,15 +168,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
every { vaultTimeoutAction = any() } just runs
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.VaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT),
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
vaultTimeoutAction = VaultTimeoutAction.LOGOUT,
|
||||
|
|
Loading…
Reference in a new issue