mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
PM-25125: Refactor user state managment into UserStateManager (#5774)
This commit is contained in:
parent
ff23dc3ab2
commit
dc198eaf72
@ -0,0 +1,43 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the global state of all users.
|
||||
*/
|
||||
interface UserStateManager {
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var hasPendingAccountAddition: Boolean
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState.hasPendingAccountAddition] flag.
|
||||
*/
|
||||
val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Tracks whether there is an account that is pending deletion in order to allow the account to
|
||||
* remain active until the deletion is finalized.
|
||||
*/
|
||||
var hasPendingAccountDeletion: Boolean
|
||||
|
||||
/**
|
||||
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
|
||||
* where many individual changes might occur that would normally affect the [UserState] but we
|
||||
* only want a single final emission. In the rare case that multiple threads are running
|
||||
* transactions simultaneously, there will be no [UserState] updates until the last
|
||||
* transaction completes.
|
||||
*/
|
||||
suspend fun <T> userStateTransaction(block: suspend () -> T): T
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* The default implementation of the [UserStateManager].
|
||||
*/
|
||||
class UserStateManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : UserStateManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
//region Pending Account Addition
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
|
||||
get() = mutableHasPendingAccountAdditionStateFlow
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
//endregion Pending Account Addition
|
||||
|
||||
//region Pending Account Deletion
|
||||
/**
|
||||
* If there is a pending account deletion, continue showing the original UserState until it
|
||||
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
|
||||
* whenever set to `true`.
|
||||
*/
|
||||
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override var hasPendingAccountDeletion: Boolean
|
||||
by mutableHasPendingAccountDeletionStateFlow::value
|
||||
//endregion Pending Account Deletion
|
||||
|
||||
/**
|
||||
* Whenever a function needs to update multiple underlying data-points that contribute to the
|
||||
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
|
||||
* until the transaction is complete. This is accomplished by blocking the emissions of the
|
||||
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
|
||||
* process is updating data simultaneously).
|
||||
*/
|
||||
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultLockManager.vaultUnlockDataStateFlow,
|
||||
hasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
merge(
|
||||
mutableHasPendingAccountDeletionStateFlow,
|
||||
mutableUserStateTransactionCountStateFlow,
|
||||
vaultLockManager.isActiveUserUnlockingFlow,
|
||||
),
|
||||
) { array ->
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot {
|
||||
mutableHasPendingAccountDeletionStateFlow.value ||
|
||||
mutableUserStateTransactionCountStateFlow.value > 0 ||
|
||||
vaultLockManager.isActiveUserUnlockingFlow.value
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultLockManager.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun <T> userStateTransaction(block: suspend () -> T): T {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.inc() }
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.dec() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBiometricsEnabled(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
|
||||
|
||||
private fun isDeviceTrusted(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType = authDiskSource
|
||||
.getPinProtectedUserKey(userId = userId)
|
||||
?.let { VaultUnlockType.PIN }
|
||||
?: VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
@ -6,6 +6,7 @@ import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
@ -27,7 +28,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
@ -44,17 +44,12 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
* Provides an API for observing an modifying authentication state.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
|
||||
/**
|
||||
* Models the current auth state.
|
||||
*/
|
||||
val authStateFlow: StateFlow<AuthState>
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||
@ -110,15 +105,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
var shouldTrustDevice: Boolean
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var hasPendingAccountAddition: Boolean
|
||||
|
||||
/**
|
||||
* Return the cached password policies for the current user.
|
||||
*/
|
||||
@ -140,11 +126,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
val showWelcomeCarousel: Boolean
|
||||
|
||||
/**
|
||||
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
||||
*/
|
||||
fun clearPendingAccountDeletion()
|
||||
|
||||
/**
|
||||
* Attempt to delete the current account using the [masterPassword] and log them out
|
||||
* upon success.
|
||||
|
||||
@ -46,7 +46,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
@ -55,6 +54,7 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
@ -77,13 +77,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
@ -91,36 +86,25 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -128,13 +112,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -144,7 +126,6 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -172,12 +153,13 @@ class AuthRepositoryImpl(
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
private val userStateManager: UserStateManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager {
|
||||
AuthRequestManager by authRequestManager,
|
||||
UserStateManager by userStateManager {
|
||||
/**
|
||||
* A scope intended for use when simply collecting multiple flows in order to combine them. The
|
||||
* use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
|
||||
@ -190,24 +172,6 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* If there is a pending account deletion, continue showing the original UserState until it
|
||||
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
|
||||
* whenever set to `true`.
|
||||
*/
|
||||
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* Whenever a function needs to update multiple underlying data-points that contribute to the
|
||||
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
|
||||
* until the transaction is complete. This is accomplished by blocking the emissions of the
|
||||
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
|
||||
* process is updating data simultaneously).
|
||||
*/
|
||||
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
|
||||
|
||||
/**
|
||||
* The auth information to make the identity token request will need to be
|
||||
* cached to make the request again in the case of two-factor authentication.
|
||||
@ -268,68 +232,6 @@ class AuthRepositoryImpl(
|
||||
initialValue = AuthState.Uninitialized,
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
merge(
|
||||
mutableHasPendingAccountDeletionStateFlow,
|
||||
mutableUserStateTransactionCountStateFlow,
|
||||
vaultRepository.isActiveUserUnlockingFlow,
|
||||
),
|
||||
) { array ->
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot {
|
||||
mutableHasPendingAccountDeletionStateFlow.value ||
|
||||
mutableUserStateTransactionCountStateFlow.value > 0 ||
|
||||
vaultRepository.isActiveUserUnlockingFlow.value
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
|
||||
|
||||
@ -358,9 +260,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
|
||||
override val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||
get() = policyManager.getActivePolicies()
|
||||
|
||||
@ -379,7 +278,7 @@ class AuthRepositoryImpl(
|
||||
|
||||
init {
|
||||
combine(
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
userStateManager.hasPendingAccountAdditionStateFlow,
|
||||
authDiskSource.userStateFlow,
|
||||
environmentRepository.environmentStateFlow,
|
||||
) { hasPendingAddition, userState, environment ->
|
||||
@ -460,16 +359,12 @@ class AuthRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
override fun clearPendingAccountDeletion() {
|
||||
mutableHasPendingAccountDeletionStateFlow.value = false
|
||||
}
|
||||
|
||||
override suspend fun deleteAccountWithMasterPassword(
|
||||
masterPassword: String,
|
||||
): DeleteAccountResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
return authSdkSource
|
||||
.hashPassword(
|
||||
email = profile.email,
|
||||
@ -489,7 +384,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun deleteAccountWithOneTimePassword(
|
||||
oneTimePassword: String,
|
||||
): DeleteAccountResult {
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
return accountsService
|
||||
.deleteAccount(
|
||||
masterPasswordHash = null,
|
||||
@ -501,13 +396,13 @@ class AuthRepositoryImpl(
|
||||
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
|
||||
fold(
|
||||
onFailure = {
|
||||
clearPendingAccountDeletion()
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
DeleteAccountResult.Error(error = it, message = null)
|
||||
},
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is DeleteAccountResponseJson.Invalid -> {
|
||||
clearPendingAccountDeletion()
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
DeleteAccountResult.Error(message = response.message, error = null)
|
||||
}
|
||||
|
||||
@ -874,7 +769,7 @@ class AuthRepositoryImpl(
|
||||
// We need to make sure that the environment is set back to the correct spot.
|
||||
updateEnvironment()
|
||||
// No switching to do but clear any pending account additions
|
||||
hasPendingAccountAddition = false
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
return SwitchAccountResult.NoChange
|
||||
}
|
||||
|
||||
@ -889,7 +784,7 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
||||
|
||||
// Clear any pending account additions
|
||||
hasPendingAccountAddition = false
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
|
||||
return SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
@ -1552,27 +1447,6 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private fun isBiometricsEnabled(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
|
||||
|
||||
private fun isDeviceTrusted(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType =
|
||||
when {
|
||||
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
|
||||
VaultUnlockType.PIN
|
||||
}
|
||||
|
||||
else -> {
|
||||
VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the saved state with the force password reset reason.
|
||||
*/
|
||||
@ -1683,7 +1557,7 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel?,
|
||||
orgIdentifier: String?,
|
||||
userConfirmedKeyConnector: Boolean,
|
||||
): LoginResult = userStateTransaction {
|
||||
): LoginResult = userStateManager.userStateTransaction {
|
||||
val userStateJson = loginResponse.toUserState(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
@ -1722,7 +1596,7 @@ class AuthRepositoryImpl(
|
||||
// we should ask him to confirm the domain
|
||||
if (isNewKeyConnectorUser && isNotConfirmed) {
|
||||
keyConnectorResponse = loginResponse
|
||||
return LoginResult.ConfirmKeyConnectorDomain(
|
||||
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
|
||||
domain = keyConnectorUrl,
|
||||
)
|
||||
}
|
||||
@ -2156,22 +2030,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
//endregion LoginCommon
|
||||
|
||||
/**
|
||||
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
|
||||
* where many individual changes might occur that would normally affect the [UserState] but we
|
||||
* only want a single final emission. In the rare case that multiple threads are running
|
||||
* transactions simultaneously, there will be no [UserState] updates until the last
|
||||
* transaction completes.
|
||||
*/
|
||||
private inline fun <T> userStateTransaction(block: () -> T): T {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.inc() }
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.dec() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,6 +13,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
@ -22,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@ -60,8 +63,8 @@ object AuthRepositoryModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
userStateManager: UserStateManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
clock = clock,
|
||||
accountsService = accountsService,
|
||||
@ -83,7 +86,21 @@ object AuthRepositoryModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
logsManager = logsManager,
|
||||
userStateManager = userStateManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesUserStateManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): UserStateManager = UserStateManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ class DeleteAccountViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleAccountDeletionConfirm() {
|
||||
authRepository.clearPendingAccountDeletion()
|
||||
authRepository.hasPendingAccountDeletion = false
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@ class DeleteAccountConfirmationViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleDeleteAccountAcknowledge() {
|
||||
authRepository.clearPendingAccountDeletion()
|
||||
authRepository.hasPendingAccountDeletion = false
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,342 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.GetTokenResponseJson
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.createMockOrganization
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertNull
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class UserStateManagerTest {
|
||||
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val firstTimeActionManager = mockk<FirstTimeActionManager> {
|
||||
every { currentOrDefaultUserFirstTimeState } returns FIRST_TIME_STATE
|
||||
every { firstTimeStateFlow } returns MutableStateFlow(FIRST_TIME_STATE)
|
||||
}
|
||||
private val mutableVaultUnlockDataStateFlow = MutableStateFlow(VAULT_UNLOCK_DATA)
|
||||
private val mutableIsActiveUserUnlockingFlow = MutableStateFlow(false)
|
||||
private val vaultLockManager: VaultLockManager = mockk {
|
||||
every { vaultUnlockDataStateFlow } returns mutableVaultUnlockDataStateFlow
|
||||
every { isActiveUserUnlockingFlow } returns mutableIsActiveUserUnlockingFlow
|
||||
}
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
|
||||
private val userStateManager: UserStateManager = UserStateManagerImpl(
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(GetTokenResponseJson.Success::toUserState)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(GetTokenResponseJson.Success::toUserState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userStateFlow should update according to changes in its underlying data sources`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
userStateManager.userStateFlow.test {
|
||||
assertNull(awaitItem())
|
||||
|
||||
mutableVaultUnlockDataStateFlow.value = VAULT_UNLOCK_DATA
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
onboardingStatus = null,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.apply {
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_1,
|
||||
pinProtectedUserKey = "pinProtectedUseKey",
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_2,
|
||||
pinProtectedUserKey = "pinProtectedUseKey",
|
||||
)
|
||||
userState = MULTI_USER_STATE
|
||||
}
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
val emptyVaultState = emptyList<VaultUnlockData>()
|
||||
mutableVaultUnlockDataStateFlow.value = emptyVaultState
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = emptyVaultState,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.apply {
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_1,
|
||||
pinProtectedUserKey = null,
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_2,
|
||||
pinProtectedUserKey = null,
|
||||
)
|
||||
storeOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS,
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = emptyVaultState,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = USER_ORGANIZATIONS,
|
||||
userIsUsingKeyConnectorList = USER_SHOULD_USER_KEY_CONNECTOR,
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear Pending Account Deletion should unblock userState updates`() = runTest {
|
||||
val originalUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
val finalUserState = SINGLE_USER_STATE_2.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
userStateManager.userStateFlow.test {
|
||||
assertEquals(originalUserState, awaitItem())
|
||||
|
||||
// Set the pending deletion flag
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
|
||||
// Update the account. No changes are emitted because
|
||||
// the pending deletion blocks the update.
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
expectNoEvents()
|
||||
|
||||
// Clearing the pending deletion allows the change to go through
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
assertEquals(finalUserState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasPendingAccountAdditionStateFlow updates when hasPendingAccountAddition changes`() =
|
||||
runTest {
|
||||
userStateManager.hasPendingAccountAdditionStateFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
userStateManager.hasPendingAccountAddition = true
|
||||
assertTrue(awaitItem())
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasPendingAccountAddition updates when hasPendingAccountAddition changes`() {
|
||||
assertFalse(userStateManager.hasPendingAccountAddition)
|
||||
userStateManager.hasPendingAccountAddition = true
|
||||
assertTrue(userStateManager.hasPendingAccountAddition)
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
assertFalse(userStateManager.hasPendingAccountAddition)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasPendingAccountDeletion updates when hasPendingAccountDeletion changes`() {
|
||||
assertFalse(userStateManager.hasPendingAccountDeletion)
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
assertTrue(userStateManager.hasPendingAccountDeletion)
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
assertFalse(userStateManager.hasPendingAccountDeletion)
|
||||
}
|
||||
}
|
||||
|
||||
private const val EMAIL_1 = "test@bitwarden.com"
|
||||
private const val EMAIL_2 = "test2@bitwarden.com"
|
||||
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
|
||||
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
|
||||
|
||||
private val FIRST_TIME_STATE = FirstTimeState(
|
||||
showImportLoginsCard = true,
|
||||
)
|
||||
|
||||
private val ORGANIZATIONS = listOf(createMockOrganization(number = 0))
|
||||
private val USER_ORGANIZATIONS = listOf(
|
||||
UserOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS.toOrganizations(),
|
||||
),
|
||||
)
|
||||
private val USER_SHOULD_USER_KEY_CONNECTOR = listOf(
|
||||
UserKeyConnectorState(
|
||||
userId = USER_ID_1,
|
||||
isUsingKeyConnector = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val VAULT_UNLOCK_DATA = listOf(
|
||||
VaultUnlockData(
|
||||
userId = USER_ID_1,
|
||||
status = VaultUnlockData.Status.UNLOCKED,
|
||||
),
|
||||
)
|
||||
|
||||
private val PROFILE_1 = AccountJson.Profile(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL_1,
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
private val ACCOUNT_1 = AccountJson(
|
||||
profile = PROFILE_1,
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
),
|
||||
)
|
||||
private val ACCOUNT_2 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_2,
|
||||
email = EMAIL_2,
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester 2",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 400000,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_1 = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_2 = UserStateJson(
|
||||
activeUserId = USER_ID_2,
|
||||
accounts = mapOf(
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
private val MULTI_USER_STATE = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
@ -76,6 +76,7 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
@ -98,17 +99,13 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
@ -116,11 +113,9 @@ import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
@ -136,6 +131,7 @@ import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -253,14 +249,21 @@ class AuthRepositoryTest {
|
||||
getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD)
|
||||
} returns mutableActivePolicyFlow
|
||||
}
|
||||
|
||||
private val firstTimeActionManager = mockk<FirstTimeActionManager> {
|
||||
every { currentOrDefaultUserFirstTimeState } returns FIRST_TIME_STATE
|
||||
every { firstTimeStateFlow } returns MutableStateFlow(FIRST_TIME_STATE)
|
||||
}
|
||||
private val logsManager: LogsManager = mockk {
|
||||
every { setUserData(userId = any(), environmentType = any()) } just runs
|
||||
}
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
|
||||
private val userStateManager: UserStateManager = mockk {
|
||||
every {
|
||||
hasPendingAccountAdditionStateFlow
|
||||
} returns mutableHasPendingAccountAdditionStateFlow
|
||||
every { hasPendingAccountAddition = any() } just runs
|
||||
every { hasPendingAccountAddition } returns mutableHasPendingAccountAdditionStateFlow.value
|
||||
every { hasPendingAccountDeletion = any() } just runs
|
||||
val blockSlot = slot<suspend () -> LoginResult>()
|
||||
coEvery { userStateTransaction(capture(blockSlot)) } coAnswers { blockSlot.captured() }
|
||||
}
|
||||
|
||||
private val repository: AuthRepository = AuthRepositoryImpl(
|
||||
clock = FIXED_CLOCK,
|
||||
accountsService = accountsService,
|
||||
@ -282,8 +285,8 @@ class AuthRepositoryTest {
|
||||
dispatcherManager = dispatcherManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
logsManager = logsManager,
|
||||
userStateManager = userStateManager,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
@ -362,108 +365,6 @@ class AuthRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userStateFlow should update according to changes in its underlying data sources`() {
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertEquals(
|
||||
null,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
mutableVaultUnlockDataStateFlow.value = VAULT_UNLOCK_DATA
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
onboardingStatus = null,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.apply {
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_1,
|
||||
pinProtectedUserKey = "pinProtectedUseKey",
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_2,
|
||||
pinProtectedUserKey = "pinProtectedUseKey",
|
||||
)
|
||||
userState = MULTI_USER_STATE
|
||||
}
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
val emptyVaultState = emptyList<VaultUnlockData>()
|
||||
mutableVaultUnlockDataStateFlow.value = emptyVaultState
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = emptyVaultState,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.apply {
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_1,
|
||||
pinProtectedUserKey = null,
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_2,
|
||||
pinProtectedUserKey = null,
|
||||
)
|
||||
storeOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS,
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = emptyVaultState,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = USER_ORGANIZATIONS,
|
||||
userIsUsingKeyConnectorList = USER_SHOULD_USER_KEY_CONNECTOR,
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Suppress("MaxLineLength")
|
||||
@ -593,7 +494,10 @@ class AuthRepositoryTest {
|
||||
),
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -688,63 +592,6 @@ class AuthRepositoryTest {
|
||||
assertEquals(ORGANIZATIONS, repository.organizations)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear Pending Account Deletion should unblock userState updates`() = runTest {
|
||||
val masterPassword = "hello world"
|
||||
val hashedMasterPassword = "dlrow olleh"
|
||||
val originalUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
val finalUserState = SINGLE_USER_STATE_2.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams()
|
||||
coEvery {
|
||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||
} returns hashedMasterPassword.asSuccess()
|
||||
coEvery {
|
||||
accountsService.deleteAccount(
|
||||
masterPasswordHash = hashedMasterPassword,
|
||||
oneTimePassword = null,
|
||||
)
|
||||
} returns DeleteAccountResponseJson.Success.asSuccess()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
repository.userStateFlow.test {
|
||||
assertEquals(originalUserState, awaitItem())
|
||||
|
||||
// Deleting the account sets the pending deletion flag
|
||||
repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||
|
||||
// Update the account. No changes are emitted because
|
||||
// the pending deletion blocks the update.
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
expectNoEvents()
|
||||
|
||||
// Clearing the pending deletion allows the change to go through
|
||||
repository.clearPendingAccountDeletion()
|
||||
assertEquals(finalUserState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete account fails if not logged in`() = runTest {
|
||||
val masterPassword = "hello world"
|
||||
@ -768,6 +615,9 @@ class AuthRepositoryTest {
|
||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||
|
||||
assertEquals(DeleteAccountResult.Error(message = null, error = error), result)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
}
|
||||
coVerify {
|
||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||
}
|
||||
@ -793,6 +643,9 @@ class AuthRepositoryTest {
|
||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||
|
||||
assertEquals(DeleteAccountResult.Error(message = null, error = error), result)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
}
|
||||
coVerify {
|
||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||
accountsService.deleteAccount(
|
||||
@ -824,6 +677,9 @@ class AuthRepositoryTest {
|
||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||
|
||||
assertEquals(DeleteAccountResult.Error(message = "Fail", error = null), result)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
}
|
||||
coVerify {
|
||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||
accountsService.deleteAccount(
|
||||
@ -852,6 +708,9 @@ class AuthRepositoryTest {
|
||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||
|
||||
assertEquals(DeleteAccountResult.Success, result)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
}
|
||||
coVerify {
|
||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||
accountsService.deleteAccount(
|
||||
@ -877,6 +736,9 @@ class AuthRepositoryTest {
|
||||
)
|
||||
|
||||
assertEquals(DeleteAccountResult.Success, result)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
}
|
||||
coVerify {
|
||||
accountsService.deleteAccount(
|
||||
masterPasswordHash = null,
|
||||
@ -2008,7 +1870,10 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -2188,7 +2053,10 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
@ -2314,7 +2182,11 @@ class AuthRepositoryTest {
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
assertFalse(repository.hasPendingAccountAddition)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition
|
||||
userStateManager.hasPendingAccountAddition = true
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -2450,6 +2322,9 @@ class AuthRepositoryTest {
|
||||
email = EMAIL,
|
||||
twoFactorToken = "twoFactorTokenToStore",
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -2657,7 +2532,8 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify {
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId = USER_ID_1)
|
||||
}
|
||||
@ -2912,6 +2788,9 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -3021,7 +2900,10 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -3176,6 +3058,9 @@ class AuthRepositoryTest {
|
||||
email = EMAIL,
|
||||
twoFactorToken = "twoFactorTokenToStore",
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -3317,7 +3202,10 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -3376,6 +3264,7 @@ class AuthRepositoryTest {
|
||||
}
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -3544,6 +3433,7 @@ class AuthRepositoryTest {
|
||||
}
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -3749,6 +3639,7 @@ class AuthRepositoryTest {
|
||||
}
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -3896,6 +3787,7 @@ class AuthRepositoryTest {
|
||||
}
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -4005,7 +3897,10 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -4065,6 +3960,7 @@ class AuthRepositoryTest {
|
||||
vaultRepository.syncIfNecessary()
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -4175,6 +4071,7 @@ class AuthRepositoryTest {
|
||||
vaultRepository.syncIfNecessary()
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -4287,6 +4184,7 @@ class AuthRepositoryTest {
|
||||
vaultRepository.syncIfNecessary()
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
@ -4359,7 +4257,11 @@ class AuthRepositoryTest {
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
assertFalse(repository.hasPendingAccountAddition)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -4489,6 +4391,9 @@ class AuthRepositoryTest {
|
||||
email = EMAIL,
|
||||
twoFactorToken = "twoFactorTokenToStore",
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -4557,7 +4462,10 @@ class AuthRepositoryTest {
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -6008,38 +5916,19 @@ class AuthRepositoryTest {
|
||||
val updatedUserId = USER_ID_2
|
||||
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertNull(repository.userStateFlow.value)
|
||||
|
||||
assertEquals(
|
||||
SwitchAccountResult.NoChange,
|
||||
repository.switchAccount(userId = updatedUserId),
|
||||
)
|
||||
|
||||
assertNull(repository.userStateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `switchAccount when the given userId is the same as the current activeUserId should reset any pending account additions`() {
|
||||
val originalUserId = USER_ID_1
|
||||
val originalUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeEnvironmentRepository.environment = Environment.Eu
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
repository.hasPendingAccountAddition = true
|
||||
|
||||
assertEquals(
|
||||
@ -6047,46 +5936,24 @@ class AuthRepositoryTest {
|
||||
repository.switchAccount(userId = originalUserId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
assertFalse(repository.hasPendingAccountAddition)
|
||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `switchAccount when the given userId does not correspond to a saved account should do nothing`() {
|
||||
val invalidId = "invalidId"
|
||||
val originalUserState = SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeEnvironmentRepository.environment = Environment.Eu
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
SwitchAccountResult.NoChange,
|
||||
repository.switchAccount(userId = invalidId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@ -6094,24 +5961,8 @@ class AuthRepositoryTest {
|
||||
@Test
|
||||
fun `switchAccount when the userId is valid should update the current UserState and reset any pending account additions`() {
|
||||
val updatedUserId = USER_ID_2
|
||||
val originalUserState = MULTI_USER_STATE.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = emptyList(),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
onboardingStatus = null,
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
)
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
fakeEnvironmentRepository.environment = Environment.Eu
|
||||
assertEquals(
|
||||
originalUserState,
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
repository.hasPendingAccountAddition = true
|
||||
|
||||
assertEquals(
|
||||
@ -6119,12 +5970,10 @@ class AuthRepositoryTest {
|
||||
repository.switchAccount(userId = updatedUserId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
originalUserState.copy(activeUserId = updatedUserId),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
assertFalse(repository.hasPendingAccountAddition)
|
||||
assertEquals(Environment.Eu, fakeEnvironmentRepository.environment)
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -6888,6 +6737,9 @@ class AuthRepositoryTest {
|
||||
)
|
||||
// This should only be set after they complete a registration and not based on login.
|
||||
assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1))
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -6956,7 +6808,10 @@ class AuthRepositoryTest {
|
||||
userId = USER_ID_1,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
verify(exactly = 1) {
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1))
|
||||
}
|
||||
|
||||
@ -7014,42 +6869,6 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isUserManagedByOrganization should return true if any org userIsClaimedByOrganization is true`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.storeUserKey(userId = USER_ID_1, userKey = ENCRYPTED_USER_KEY)
|
||||
val organizations = listOf(
|
||||
createMockOrganization(number = 0)
|
||||
.copy(
|
||||
userIsClaimedByOrganization = true,
|
||||
),
|
||||
createMockOrganization(number = 1),
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations)
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1.toUserState(
|
||||
vaultState = VAULT_UNLOCK_DATA,
|
||||
userAccountTokens = emptyList(),
|
||||
userOrganizationsList = listOf(
|
||||
UserOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = organizations.toOrganizations(),
|
||||
),
|
||||
),
|
||||
userIsUsingKeyConnectorList = emptyList(),
|
||||
hasPendingAccountAddition = false,
|
||||
onboardingStatus = null,
|
||||
isBiometricsEnabledProvider = { false },
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
isDeviceTrustedProvider = { false },
|
||||
firstTimeState = FIRST_TIME_STATE,
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
@ -7259,18 +7078,6 @@ class AuthRepositoryTest {
|
||||
accessToken = ACCESS_TOKEN_2,
|
||||
refreshToken = "refreshToken",
|
||||
)
|
||||
private val USER_ORGANIZATIONS = listOf(
|
||||
UserOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS.toOrganizations(),
|
||||
),
|
||||
)
|
||||
private val USER_SHOULD_USER_KEY_CONNECTOR = listOf(
|
||||
UserKeyConnectorState(
|
||||
userId = USER_ID_1,
|
||||
isUsingKeyConnector = null,
|
||||
),
|
||||
)
|
||||
private val VAULT_UNLOCK_DATA = listOf(
|
||||
VaultUnlockData(
|
||||
userId = USER_ID_1,
|
||||
@ -7278,10 +7085,6 @@ class AuthRepositoryTest {
|
||||
),
|
||||
)
|
||||
|
||||
private val FIRST_TIME_STATE = FirstTimeState(
|
||||
showImportLoginsCard = true,
|
||||
)
|
||||
|
||||
private val SERVER_CONFIG_DEFAULT = ServerConfig(
|
||||
lastSync = 0L,
|
||||
serverData = ConfigResponseJson(
|
||||
|
||||
@ -186,7 +186,7 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `AccountDeletionConfirm should clear dialog state and call clearPendingAccountDeletion`() =
|
||||
runTest {
|
||||
every { authRepo.clearPendingAccountDeletion() } just runs
|
||||
every { authRepo.hasPendingAccountDeletion = false } just runs
|
||||
val state = DEFAULT_STATE.copy(
|
||||
dialog = DeleteAccountState.DeleteAccountDialog.DeleteSuccess,
|
||||
)
|
||||
@ -198,7 +198,7 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify {
|
||||
authRepo.clearPendingAccountDeletion()
|
||||
authRepo.hasPendingAccountDeletion = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ class DeleteAccountConfirmationViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `DeleteAccountAcknowledge should clear dialog and call clearPendingAccountDeletion`() =
|
||||
runTest {
|
||||
every { authRepo.clearPendingAccountDeletion() } just runs
|
||||
every { authRepo.hasPendingAccountDeletion = false } just runs
|
||||
val state = DEFAULT_STATE.copy(
|
||||
dialog =
|
||||
DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess(),
|
||||
@ -68,7 +68,7 @@ class DeleteAccountConfirmationViewModelTest : BaseViewModelTest() {
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify {
|
||||
authRepo.clearPendingAccountDeletion()
|
||||
authRepo.hasPendingAccountDeletion = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user