Restart activity to clear out in-memory data when locking or changing user (#1388)

This commit is contained in:
David Perez 2024-05-24 15:03:08 -05:00 committed by Álison Fernandes
parent 5a908c1d01
commit d3a1e0b6ed
7 changed files with 262 additions and 0 deletions

View file

@ -94,6 +94,8 @@ class MainActivity : AppCompatActivity() {
handleCompleteAutofill(event) handleCompleteAutofill(event)
} }
MainEvent.Recreate -> handleRecreate()
is MainEvent.ScreenCaptureSettingChange -> { is MainEvent.ScreenCaptureSettingChange -> {
handleScreenCaptureSettingChange(event) handleScreenCaptureSettingChange(event)
} }
@ -116,4 +118,8 @@ class MainActivity : AppCompatActivity() {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} }
} }
private fun handleRecreate() {
recreate()
}
} }

View file

@ -5,20 +5,28 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.CipherView import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -29,12 +37,16 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
/** /**
* A view model that helps launch actions for the [MainActivity]. * A view model that helps launch actions for the [MainActivity].
*/ */
@Suppress("LongParameterList")
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val autofillSelectionManager: AutofillSelectionManager, private val autofillSelectionManager: AutofillSelectionManager,
private val specialCircumstanceManager: SpecialCircumstanceManager, private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val intentManager: IntentManager, private val intentManager: IntentManager,
authRepository: AuthRepository,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
) : BaseViewModel<MainState, MainEvent, MainAction>( ) : BaseViewModel<MainState, MainEvent, MainAction>(
MainState( MainState(
@ -72,6 +84,36 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed)) sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed))
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
authRepository
.userStateFlow
.drop(count = 1)
// Trigger an action whenever the current user changes or we go into/out of a pending
// account state (which acts like switching to a temporary user).
.map { it?.activeUserId to it?.hasPendingAccountAddition }
.distinctUntilChanged()
.onEach {
// Switching between account states often involves some kind of animation (ex:
// account switcher) that we might want to give time to finish before triggering
// a refresh.
@Suppress("MagicNumber")
delay(500)
trySendAction(MainAction.Internal.CurrentUserStateChange)
}
.launchIn(viewModelScope)
vaultRepository
.vaultStateEventFlow
.onEach {
when (it) {
is VaultStateEvent.Locked -> {
trySendAction(MainAction.Internal.VaultUnlockStateChange)
}
is VaultStateEvent.Unlocked -> Unit
}
}
.launchIn(viewModelScope)
} }
override fun handleAction(action: MainAction) { override fun handleAction(action: MainAction) {
@ -80,7 +122,9 @@ class MainViewModel @Inject constructor(
handleAutofillSelectionReceive(action) handleAutofillSelectionReceive(action)
} }
is MainAction.Internal.CurrentUserStateChange -> handleCurrentUserStateChange()
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action) is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action) is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action) is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
} }
@ -92,10 +136,18 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView)) sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
} }
private fun handleCurrentUserStateChange() {
recreateUiAndGarbageCollect()
}
private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) { private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
mutableStateFlow.update { it.copy(theme = action.theme) } mutableStateFlow.update { it.copy(theme = action.theme) }
} }
private fun handleVaultUnlockStateChange() {
recreateUiAndGarbageCollect()
}
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) { private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
handleIntent( handleIntent(
intent = action.intent, intent = action.intent,
@ -168,6 +220,11 @@ class MainViewModel @Inject constructor(
} }
} }
} }
private fun recreateUiAndGarbageCollect() {
sendEvent(MainEvent.Recreate)
garbageCollectionManager.tryCollect()
}
} }
/** /**
@ -203,12 +260,22 @@ sealed class MainAction {
val cipherView: CipherView, val cipherView: CipherView,
) : Internal() ) : Internal()
/**
* Indicates a relevant change in the current user state.
*/
data object CurrentUserStateChange : Internal()
/** /**
* Indicates that the app theme has changed. * Indicates that the app theme has changed.
*/ */
data class ThemeUpdate( data class ThemeUpdate(
val theme: AppTheme, val theme: AppTheme,
) : Internal() ) : Internal()
/**
* Indicates a relevant change in the current vault lock state.
*/
data object VaultUnlockStateChange : Internal()
} }
} }
@ -226,4 +293,9 @@ sealed class MainEvent {
* Event indicating a change in the screen capture setting. * Event indicating a change in the screen capture setting.
*/ */
data class ScreenCaptureSettingChange(val isAllowed: Boolean) : MainEvent() data class ScreenCaptureSettingChange(val isAllowed: Boolean) : MainEvent()
/**
* Event indicating that the UI should recreate itself.
*/
data object Recreate : MainEvent()
} }

View file

@ -3,8 +3,10 @@ package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.crypto.Kdf import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.ClientAuth import com.bitwarden.sdk.ClientAuth
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
/** /**
@ -16,6 +18,11 @@ interface VaultLockManager {
*/ */
val vaultUnlockDataStateFlow: StateFlow<List<VaultUnlockData>> val vaultUnlockDataStateFlow: StateFlow<List<VaultUnlockData>>
/**
* Flow that emits whenever any vault is locked or unlocked.
*/
val vaultStateEventFlow: Flow<VaultStateEvent>
/** /**
* Whether or not the vault is currently locked for the given [userId]. * Whether or not the vault is currently locked for the given [userId].
*/ */

View file

@ -18,10 +18,12 @@ 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.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
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
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.statusFor import com.x8bit.bitwarden.data.vault.repository.util.statusFor
@ -29,8 +31,10 @@ import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.update import com.x8bit.bitwarden.data.vault.repository.util.update
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -74,10 +78,14 @@ class VaultLockManagerImpl(
private val mutableVaultUnlockDataStateFlow = private val mutableVaultUnlockDataStateFlow =
MutableStateFlow<List<VaultUnlockData>>(emptyList()) MutableStateFlow<List<VaultUnlockData>>(emptyList())
private val mutableVaultStateEventSharedFlow = bufferedMutableSharedFlow<VaultStateEvent>()
override val vaultUnlockDataStateFlow: StateFlow<List<VaultUnlockData>> override val vaultUnlockDataStateFlow: StateFlow<List<VaultUnlockData>>
get() = mutableVaultUnlockDataStateFlow.asStateFlow() get() = mutableVaultUnlockDataStateFlow.asStateFlow()
override val vaultStateEventFlow: Flow<VaultStateEvent>
get() = mutableVaultStateEventSharedFlow.asSharedFlow()
init { init {
observeAppForegroundChanges() observeAppForegroundChanges()
observeUserSwitchingChanges() observeUserSwitchingChanges()
@ -230,15 +238,20 @@ class VaultLockManagerImpl(
} }
private fun setVaultToUnlocked(userId: String) { private fun setVaultToUnlocked(userId: String) {
val wasVaultUnlocked = isVaultUnlocked(userId = userId)
mutableVaultUnlockDataStateFlow.update { mutableVaultUnlockDataStateFlow.update {
it.update(userId, VaultUnlockData.Status.UNLOCKED) it.update(userId, VaultUnlockData.Status.UNLOCKED)
} }
// If we are unlocking an account with a timeout of Never, we should make sure to store the // If we are unlocking an account with a timeout of Never, we should make sure to store the
// auto-unlock key. // auto-unlock key.
storeUserAutoUnlockKeyIfNecessary(userId = userId) storeUserAutoUnlockKeyIfNecessary(userId = userId)
if (!wasVaultUnlocked) {
mutableVaultStateEventSharedFlow.tryEmit(VaultStateEvent.Unlocked(userId = userId))
}
} }
private fun setVaultToLocked(userId: String) { private fun setVaultToLocked(userId: String) {
val wasVaultLocked = !isVaultUnlocked(userId = userId) && !isVaultUnlocking(userId = userId)
vaultSdkSource.clearCrypto(userId = userId) vaultSdkSource.clearCrypto(userId = userId)
mutableVaultUnlockDataStateFlow.update { mutableVaultUnlockDataStateFlow.update {
it.update(userId, null) it.update(userId, null)
@ -247,6 +260,9 @@ class VaultLockManagerImpl(
userId = userId, userId = userId,
userAutoUnlockKey = null, userAutoUnlockKey = null,
) )
if (!wasVaultLocked) {
mutableVaultStateEventSharedFlow.tryEmit(VaultStateEvent.Locked(userId = userId))
}
} }
private fun setVaultToUnlocking(userId: String) { private fun setVaultToUnlocking(userId: String) {

View file

@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.vault.manager.model
/**
* Represents an event that indicates if the vault for a particular user is not locked or unlocked.
*/
sealed class VaultStateEvent {
/**
* Indicates that the vault has been locked for the given [userId].
*/
data class Locked(val userId: String) : VaultStateEvent()
/**
* Indicates that the vault has been unlocked for the given [userId].
*/
data class Unlocked(val userId: String) : VaultStateEvent()
}

View file

@ -4,6 +4,8 @@ import android.content.Intent
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.CipherView import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
@ -12,17 +14,23 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -36,6 +44,10 @@ import org.junit.jupiter.api.Test
class MainViewModelTest : BaseViewModelTest() { class MainViewModelTest : BaseViewModelTest() {
private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl() private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl()
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow
}
private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT) private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT)
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true) private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
private val settingsRepository = mockk<SettingsRepository> { private val settingsRepository = mockk<SettingsRepository> {
@ -43,6 +55,13 @@ class MainViewModelTest : BaseViewModelTest() {
every { appThemeStateFlow } returns mutableAppThemeFlow every { appThemeStateFlow } returns mutableAppThemeFlow
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow
} }
private val mutableVaultStateEventFlow = bufferedMutableSharedFlow<VaultStateEvent>()
private val vaultRepository = mockk<VaultRepository> {
every { vaultStateEventFlow } returns mutableVaultStateEventFlow
}
private val garbageCollectionManager = mockk<GarbageCollectionManager> {
every { tryCollect() } just runs
}
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
private val intentManager: IntentManager = mockk { private val intentManager: IntentManager = mockk {
every { getShareDataFromIntent(any()) } returns null every { getShareDataFromIntent(any()) } returns null
@ -91,6 +110,77 @@ class MainViewModelTest : BaseViewModelTest() {
) )
} }
@Test
fun `user state updates should emit Recreate event and trigger garbage collection`() = runTest {
val userId1 = "userId1"
val userId2 = "userId12"
val viewModel = createViewModel()
viewModel.eventFlow.test {
// Ignore initial screen capture event
awaitItem()
mutableUserStateFlow.value = UserState(
activeUserId = userId1,
accounts = listOf(
mockk<UserState.Account> {
every { userId } returns userId1
},
),
hasPendingAccountAddition = false,
)
assertEquals(MainEvent.Recreate, awaitItem())
mutableUserStateFlow.value = UserState(
activeUserId = userId1,
accounts = listOf(
mockk<UserState.Account> {
every { userId } returns userId1
},
),
hasPendingAccountAddition = true,
)
assertEquals(MainEvent.Recreate, awaitItem())
mutableUserStateFlow.value = UserState(
activeUserId = userId2,
accounts = listOf(
mockk<UserState.Account> {
every { userId } returns userId1
},
mockk<UserState.Account> {
every { userId } returns userId2
},
),
hasPendingAccountAddition = true,
)
assertEquals(MainEvent.Recreate, awaitItem())
}
verify(exactly = 3) {
garbageCollectionManager.tryCollect()
}
}
@Test
fun `vault state lock events should emit Recreate event and trigger garbage collection`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
// Ignore initial screen capture event
awaitItem()
mutableVaultStateEventFlow.tryEmit(VaultStateEvent.Unlocked(userId = "userId"))
expectNoEvents()
mutableVaultStateEventFlow.tryEmit(VaultStateEvent.Locked(userId = "userId"))
assertEquals(MainEvent.Recreate, awaitItem())
}
verify(exactly = 1) {
garbageCollectionManager.tryCollect()
}
}
@Test @Test
fun `autofill selection updates should emit CompleteAutofill events`() = runTest { fun `autofill selection updates should emit CompleteAutofill events`() = runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -441,7 +531,10 @@ class MainViewModelTest : BaseViewModelTest() {
) = MainViewModel( ) = MainViewModel(
autofillSelectionManager = autofillSelectionManager, autofillSelectionManager = autofillSelectionManager,
specialCircumstanceManager = specialCircumstanceManager, specialCircumstanceManager = specialCircumstanceManager,
garbageCollectionManager = garbageCollectionManager,
authRepository = authRepository,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
intentManager = intentManager, intentManager = intentManager,
savedStateHandle = savedStateHandle.apply { savedStateHandle = savedStateHandle.apply {
set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance) set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance)

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.manager package com.x8bit.bitwarden.data.vault.manager
import app.cash.turbine.test
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
@ -22,6 +23,7 @@ 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
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
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
@ -86,6 +88,56 @@ class VaultLockManagerTest {
elapsedRealtimeMillisProvider = { elapsedRealtimeMillis }, elapsedRealtimeMillisProvider = { elapsedRealtimeMillis },
) )
@Test
fun `vaultStateEventFlow should emit Locked event when vault state changes to locked`() =
runTest {
// Ensure the vault is unlocked
verifyUnlockedVault(userId = USER_ID)
vaultLockManager.vaultStateEventFlow.test {
vaultLockManager.lockVault(userId = USER_ID)
assertEquals(VaultStateEvent.Locked(userId = USER_ID), awaitItem())
}
}
@Test
fun `vaultStateEventFlow should not emit Locked event when vault state remains locked`() =
runTest {
// Ensure the vault is locked
vaultLockManager.lockVault(userId = USER_ID)
vaultLockManager.vaultStateEventFlow.test {
vaultLockManager.lockVault(userId = USER_ID)
expectNoEvents()
}
}
@Test
fun `vaultStateEventFlow should emit Unlocked event when vault state changes to unlocked`() =
runTest {
// Ensure the vault is locked
vaultLockManager.lockVault(userId = USER_ID)
vaultLockManager.vaultStateEventFlow.test {
verifyUnlockedVault(userId = USER_ID)
assertEquals(VaultStateEvent.Unlocked(userId = USER_ID), awaitItem())
}
}
@Test
fun `vaultStateEventFlow should not emit Unlocked event when vault state remains unlocked`() =
runTest {
// Ensure the vault is unlocked
verifyUnlockedVault(userId = USER_ID)
vaultLockManager.vaultStateEventFlow.test {
// There is no great way to directly call the internal setVaultToUnlocked
// but that will be called internally again when syncing.
vaultLockManager.syncVaultState(userId = USER_ID)
expectNoEvents()
}
}
@Test @Test
fun `app going into background should update the current user's last active time`() { fun `app going into background should update the current user's last active time`() {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE