mirror of
https://github.com/bitwarden/android.git
synced 2025-12-15 16:40:20 -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.ForcePasswordResetReason
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
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.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.AuthState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
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.SendVerificationEmailResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
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.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.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
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.
|
* Provides an API for observing an modifying authentication state.
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
|
||||||
/**
|
/**
|
||||||
* Models the current auth state.
|
* Models the current auth state.
|
||||||
*/
|
*/
|
||||||
val authStateFlow: StateFlow<AuthState>
|
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
|
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||||
@ -110,15 +105,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||||||
*/
|
*/
|
||||||
var shouldTrustDevice: Boolean
|
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.
|
* Return the cached password policies for the current user.
|
||||||
*/
|
*/
|
||||||
@ -140,11 +126,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||||||
*/
|
*/
|
||||||
val showWelcomeCarousel: Boolean
|
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
|
* Attempt to delete the current account using the [masterPassword] and log them out
|
||||||
* upon success.
|
* 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.AccountTokensJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
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.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.network.model.DeviceDataModel
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
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.KeyConnectorManager
|
||||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
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.manager.model.MigrateExistingUserToKeyConnectorResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
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.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.SendVerificationEmailResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
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.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.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
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.VerifiedOrganizationDomainSsoDetailsResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
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.SsoCallbackResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
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.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.policyInformation
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
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.toSdkParams
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
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.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.repository.util.userSwitchingChangesFlow
|
||||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
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.YubiKeyResult
|
||||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
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.LogsManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
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.manager.util.getActivePolicies
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
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.VaultUnlockError
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -128,13 +112,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterNot
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -144,7 +126,6 @@ import kotlinx.coroutines.flow.merge
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -172,12 +153,13 @@ class AuthRepositoryImpl(
|
|||||||
private val trustedDeviceManager: TrustedDeviceManager,
|
private val trustedDeviceManager: TrustedDeviceManager,
|
||||||
private val userLogoutManager: UserLogoutManager,
|
private val userLogoutManager: UserLogoutManager,
|
||||||
private val policyManager: PolicyManager,
|
private val policyManager: PolicyManager,
|
||||||
firstTimeActionManager: FirstTimeActionManager,
|
private val userStateManager: UserStateManager,
|
||||||
logsManager: LogsManager,
|
logsManager: LogsManager,
|
||||||
pushManager: PushManager,
|
pushManager: PushManager,
|
||||||
dispatcherManager: DispatcherManager,
|
dispatcherManager: DispatcherManager,
|
||||||
) : AuthRepository,
|
) : 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
|
* 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
|
* 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 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
|
* 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.
|
* cached to make the request again in the case of two-factor authentication.
|
||||||
@ -268,68 +232,6 @@ class AuthRepositoryImpl(
|
|||||||
initialValue = AuthState.Uninitialized,
|
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)
|
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
|
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>
|
override val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||||
get() = policyManager.getActivePolicies()
|
get() = policyManager.getActivePolicies()
|
||||||
|
|
||||||
@ -379,7 +278,7 @@ class AuthRepositoryImpl(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
combine(
|
combine(
|
||||||
mutableHasPendingAccountAdditionStateFlow,
|
userStateManager.hasPendingAccountAdditionStateFlow,
|
||||||
authDiskSource.userStateFlow,
|
authDiskSource.userStateFlow,
|
||||||
environmentRepository.environmentStateFlow,
|
environmentRepository.environmentStateFlow,
|
||||||
) { hasPendingAddition, userState, environment ->
|
) { hasPendingAddition, userState, environment ->
|
||||||
@ -460,16 +359,12 @@ class AuthRepositoryImpl(
|
|||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearPendingAccountDeletion() {
|
|
||||||
mutableHasPendingAccountDeletionStateFlow.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteAccountWithMasterPassword(
|
override suspend fun deleteAccountWithMasterPassword(
|
||||||
masterPassword: String,
|
masterPassword: String,
|
||||||
): DeleteAccountResult {
|
): DeleteAccountResult {
|
||||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||||
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
|
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
|
||||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
return authSdkSource
|
return authSdkSource
|
||||||
.hashPassword(
|
.hashPassword(
|
||||||
email = profile.email,
|
email = profile.email,
|
||||||
@ -489,7 +384,7 @@ class AuthRepositoryImpl(
|
|||||||
override suspend fun deleteAccountWithOneTimePassword(
|
override suspend fun deleteAccountWithOneTimePassword(
|
||||||
oneTimePassword: String,
|
oneTimePassword: String,
|
||||||
): DeleteAccountResult {
|
): DeleteAccountResult {
|
||||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
return accountsService
|
return accountsService
|
||||||
.deleteAccount(
|
.deleteAccount(
|
||||||
masterPasswordHash = null,
|
masterPasswordHash = null,
|
||||||
@ -501,13 +396,13 @@ class AuthRepositoryImpl(
|
|||||||
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
|
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
|
||||||
fold(
|
fold(
|
||||||
onFailure = {
|
onFailure = {
|
||||||
clearPendingAccountDeletion()
|
userStateManager.hasPendingAccountDeletion = false
|
||||||
DeleteAccountResult.Error(error = it, message = null)
|
DeleteAccountResult.Error(error = it, message = null)
|
||||||
},
|
},
|
||||||
onSuccess = { response ->
|
onSuccess = { response ->
|
||||||
when (response) {
|
when (response) {
|
||||||
is DeleteAccountResponseJson.Invalid -> {
|
is DeleteAccountResponseJson.Invalid -> {
|
||||||
clearPendingAccountDeletion()
|
userStateManager.hasPendingAccountDeletion = false
|
||||||
DeleteAccountResult.Error(message = response.message, error = null)
|
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.
|
// We need to make sure that the environment is set back to the correct spot.
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
// No switching to do but clear any pending account additions
|
// No switching to do but clear any pending account additions
|
||||||
hasPendingAccountAddition = false
|
userStateManager.hasPendingAccountAddition = false
|
||||||
return SwitchAccountResult.NoChange
|
return SwitchAccountResult.NoChange
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -889,7 +784,7 @@ class AuthRepositoryImpl(
|
|||||||
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
||||||
|
|
||||||
// Clear any pending account additions
|
// Clear any pending account additions
|
||||||
hasPendingAccountAddition = false
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
|
||||||
return SwitchAccountResult.AccountSwitched
|
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.
|
* Update the saved state with the force password reset reason.
|
||||||
*/
|
*/
|
||||||
@ -1683,7 +1557,7 @@ class AuthRepositoryImpl(
|
|||||||
deviceData: DeviceDataModel?,
|
deviceData: DeviceDataModel?,
|
||||||
orgIdentifier: String?,
|
orgIdentifier: String?,
|
||||||
userConfirmedKeyConnector: Boolean,
|
userConfirmedKeyConnector: Boolean,
|
||||||
): LoginResult = userStateTransaction {
|
): LoginResult = userStateManager.userStateTransaction {
|
||||||
val userStateJson = loginResponse.toUserState(
|
val userStateJson = loginResponse.toUserState(
|
||||||
previousUserState = authDiskSource.userState,
|
previousUserState = authDiskSource.userState,
|
||||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||||
@ -1722,7 +1596,7 @@ class AuthRepositoryImpl(
|
|||||||
// we should ask him to confirm the domain
|
// we should ask him to confirm the domain
|
||||||
if (isNewKeyConnectorUser && isNotConfirmed) {
|
if (isNewKeyConnectorUser && isNotConfirmed) {
|
||||||
keyConnectorResponse = loginResponse
|
keyConnectorResponse = loginResponse
|
||||||
return LoginResult.ConfirmKeyConnectorDomain(
|
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
|
||||||
domain = keyConnectorUrl,
|
domain = keyConnectorUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -2156,22 +2030,6 @@ class AuthRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//endregion LoginCommon
|
//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.KeyConnectorManager
|
||||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
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.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
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.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
|
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@ -60,8 +63,8 @@ object AuthRepositoryModule {
|
|||||||
userLogoutManager: UserLogoutManager,
|
userLogoutManager: UserLogoutManager,
|
||||||
pushManager: PushManager,
|
pushManager: PushManager,
|
||||||
policyManager: PolicyManager,
|
policyManager: PolicyManager,
|
||||||
firstTimeActionManager: FirstTimeActionManager,
|
|
||||||
logsManager: LogsManager,
|
logsManager: LogsManager,
|
||||||
|
userStateManager: UserStateManager,
|
||||||
): AuthRepository = AuthRepositoryImpl(
|
): AuthRepository = AuthRepositoryImpl(
|
||||||
clock = clock,
|
clock = clock,
|
||||||
accountsService = accountsService,
|
accountsService = accountsService,
|
||||||
@ -83,7 +86,21 @@ object AuthRepositoryModule {
|
|||||||
userLogoutManager = userLogoutManager,
|
userLogoutManager = userLogoutManager,
|
||||||
pushManager = pushManager,
|
pushManager = pushManager,
|
||||||
policyManager = policyManager,
|
policyManager = policyManager,
|
||||||
firstTimeActionManager = firstTimeActionManager,
|
|
||||||
logsManager = logsManager,
|
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() {
|
private fun handleAccountDeletionConfirm() {
|
||||||
authRepository.clearPendingAccountDeletion()
|
authRepository.hasPendingAccountDeletion = false
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class DeleteAccountConfirmationViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeleteAccountAcknowledge() {
|
private fun handleDeleteAccountAcknowledge() {
|
||||||
authRepository.clearPendingAccountDeletion()
|
authRepository.hasPendingAccountDeletion = false
|
||||||
mutableStateFlow.update { it.copy(dialog = null) }
|
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.KeyConnectorManager
|
||||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
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.AuthRequest
|
||||||
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
|
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.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.SendVerificationEmailResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
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.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.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
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.VerifiedOrganizationDomainSsoDetailsResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
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.DuoCallbackTokenResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
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.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.toRemovedPasswordUserStateJson
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
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.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.auth.util.toSdkParams
|
||||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
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.LogsManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
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.manager.model.NotificationLogoutData
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||||
@ -136,6 +131,7 @@ import io.mockk.mockk
|
|||||||
import io.mockk.mockkConstructor
|
import io.mockk.mockkConstructor
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
|
import io.mockk.slot
|
||||||
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
|
||||||
@ -253,14 +249,21 @@ class AuthRepositoryTest {
|
|||||||
getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD)
|
getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD)
|
||||||
} returns mutableActivePolicyFlow
|
} 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 {
|
private val logsManager: LogsManager = mockk {
|
||||||
every { setUserData(userId = any(), environmentType = any()) } just runs
|
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(
|
private val repository: AuthRepository = AuthRepositoryImpl(
|
||||||
clock = FIXED_CLOCK,
|
clock = FIXED_CLOCK,
|
||||||
accountsService = accountsService,
|
accountsService = accountsService,
|
||||||
@ -282,8 +285,8 @@ class AuthRepositoryTest {
|
|||||||
dispatcherManager = dispatcherManager,
|
dispatcherManager = dispatcherManager,
|
||||||
pushManager = pushManager,
|
pushManager = pushManager,
|
||||||
policyManager = policyManager,
|
policyManager = policyManager,
|
||||||
firstTimeActionManager = firstTimeActionManager,
|
|
||||||
logsManager = logsManager,
|
logsManager = logsManager,
|
||||||
|
userStateManager = userStateManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@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
|
@Test
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@ -593,7 +494,10 @@ class AuthRepositoryTest {
|
|||||||
),
|
),
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -688,63 +592,6 @@ class AuthRepositoryTest {
|
|||||||
assertEquals(ORGANIZATIONS, repository.organizations)
|
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
|
@Test
|
||||||
fun `delete account fails if not logged in`() = runTest {
|
fun `delete account fails if not logged in`() = runTest {
|
||||||
val masterPassword = "hello world"
|
val masterPassword = "hello world"
|
||||||
@ -768,6 +615,9 @@ class AuthRepositoryTest {
|
|||||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||||
|
|
||||||
assertEquals(DeleteAccountResult.Error(message = null, error = error), result)
|
assertEquals(DeleteAccountResult.Error(message = null, error = error), result)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
|
}
|
||||||
coVerify {
|
coVerify {
|
||||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||||
}
|
}
|
||||||
@ -793,6 +643,9 @@ class AuthRepositoryTest {
|
|||||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||||
|
|
||||||
assertEquals(DeleteAccountResult.Error(message = null, error = error), result)
|
assertEquals(DeleteAccountResult.Error(message = null, error = error), result)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
|
}
|
||||||
coVerify {
|
coVerify {
|
||||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||||
accountsService.deleteAccount(
|
accountsService.deleteAccount(
|
||||||
@ -824,6 +677,9 @@ class AuthRepositoryTest {
|
|||||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||||
|
|
||||||
assertEquals(DeleteAccountResult.Error(message = "Fail", error = null), result)
|
assertEquals(DeleteAccountResult.Error(message = "Fail", error = null), result)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
|
}
|
||||||
coVerify {
|
coVerify {
|
||||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||||
accountsService.deleteAccount(
|
accountsService.deleteAccount(
|
||||||
@ -852,6 +708,9 @@ class AuthRepositoryTest {
|
|||||||
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
val result = repository.deleteAccountWithMasterPassword(masterPassword = masterPassword)
|
||||||
|
|
||||||
assertEquals(DeleteAccountResult.Success, result)
|
assertEquals(DeleteAccountResult.Success, result)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
|
}
|
||||||
coVerify {
|
coVerify {
|
||||||
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
authSdkSource.hashPassword(EMAIL, masterPassword, kdf, HashPurpose.SERVER_AUTHORIZATION)
|
||||||
accountsService.deleteAccount(
|
accountsService.deleteAccount(
|
||||||
@ -877,6 +736,9 @@ class AuthRepositoryTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(DeleteAccountResult.Success, result)
|
assertEquals(DeleteAccountResult.Success, result)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountDeletion = true
|
||||||
|
}
|
||||||
coVerify {
|
coVerify {
|
||||||
accountsService.deleteAccount(
|
accountsService.deleteAccount(
|
||||||
masterPasswordHash = null,
|
masterPasswordHash = null,
|
||||||
@ -2008,7 +1870,10 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -2188,7 +2053,10 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
coVerify(exactly = 0) {
|
coVerify(exactly = 0) {
|
||||||
vaultRepository.unlockVault(
|
vaultRepository.unlockVault(
|
||||||
userId = USER_ID_1,
|
userId = USER_ID_1,
|
||||||
@ -2314,7 +2182,11 @@ class AuthRepositoryTest {
|
|||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
assertFalse(repository.hasPendingAccountAddition)
|
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
|
@Test
|
||||||
@ -2450,6 +2322,9 @@ class AuthRepositoryTest {
|
|||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
twoFactorToken = "twoFactorTokenToStore",
|
twoFactorToken = "twoFactorTokenToStore",
|
||||||
)
|
)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -2657,7 +2532,8 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
settingsRepository.storeUserHasLoggedInValue(userId = USER_ID_1)
|
settingsRepository.storeUserHasLoggedInValue(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
@ -2912,6 +2788,9 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -3021,7 +2900,10 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -3176,6 +3058,9 @@ class AuthRepositoryTest {
|
|||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
twoFactorToken = "twoFactorTokenToStore",
|
twoFactorToken = "twoFactorTokenToStore",
|
||||||
)
|
)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -3317,7 +3202,10 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -3376,6 +3264,7 @@ class AuthRepositoryTest {
|
|||||||
}
|
}
|
||||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3544,6 +3433,7 @@ class AuthRepositoryTest {
|
|||||||
}
|
}
|
||||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3749,6 +3639,7 @@ class AuthRepositoryTest {
|
|||||||
}
|
}
|
||||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3896,6 +3787,7 @@ class AuthRepositoryTest {
|
|||||||
}
|
}
|
||||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4005,7 +3897,10 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -4065,6 +3960,7 @@ class AuthRepositoryTest {
|
|||||||
vaultRepository.syncIfNecessary()
|
vaultRepository.syncIfNecessary()
|
||||||
}
|
}
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4175,6 +4071,7 @@ class AuthRepositoryTest {
|
|||||||
vaultRepository.syncIfNecessary()
|
vaultRepository.syncIfNecessary()
|
||||||
}
|
}
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4287,6 +4184,7 @@ class AuthRepositoryTest {
|
|||||||
vaultRepository.syncIfNecessary()
|
vaultRepository.syncIfNecessary()
|
||||||
}
|
}
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4359,7 +4257,11 @@ class AuthRepositoryTest {
|
|||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
assertFalse(repository.hasPendingAccountAddition)
|
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
|
@Test
|
||||||
@ -4489,6 +4391,9 @@ class AuthRepositoryTest {
|
|||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
twoFactorToken = "twoFactorTokenToStore",
|
twoFactorToken = "twoFactorTokenToStore",
|
||||||
)
|
)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -4557,7 +4462,10 @@ class AuthRepositoryTest {
|
|||||||
SINGLE_USER_STATE_1,
|
SINGLE_USER_STATE_1,
|
||||||
fakeAuthDiskSource.userState,
|
fakeAuthDiskSource.userState,
|
||||||
)
|
)
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -6008,38 +5916,19 @@ class AuthRepositoryTest {
|
|||||||
val updatedUserId = USER_ID_2
|
val updatedUserId = USER_ID_2
|
||||||
|
|
||||||
fakeAuthDiskSource.userState = null
|
fakeAuthDiskSource.userState = null
|
||||||
assertNull(repository.userStateFlow.value)
|
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SwitchAccountResult.NoChange,
|
SwitchAccountResult.NoChange,
|
||||||
repository.switchAccount(userId = updatedUserId),
|
repository.switchAccount(userId = updatedUserId),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNull(repository.userStateFlow.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `switchAccount when the given userId is the same as the current activeUserId should reset any pending account additions`() {
|
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 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
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
fakeEnvironmentRepository.environment = Environment.Eu
|
fakeEnvironmentRepository.environment = Environment.Eu
|
||||||
assertEquals(
|
|
||||||
originalUserState,
|
|
||||||
repository.userStateFlow.value,
|
|
||||||
)
|
|
||||||
repository.hasPendingAccountAddition = true
|
repository.hasPendingAccountAddition = true
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -6047,46 +5936,24 @@ class AuthRepositoryTest {
|
|||||||
repository.switchAccount(userId = originalUserId),
|
repository.switchAccount(userId = originalUserId),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
originalUserState,
|
|
||||||
repository.userStateFlow.value,
|
|
||||||
)
|
|
||||||
assertFalse(repository.hasPendingAccountAddition)
|
|
||||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `switchAccount when the given userId does not correspond to a saved account should do nothing`() {
|
fun `switchAccount when the given userId does not correspond to a saved account should do nothing`() {
|
||||||
val invalidId = "invalidId"
|
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
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
fakeEnvironmentRepository.environment = Environment.Eu
|
fakeEnvironmentRepository.environment = Environment.Eu
|
||||||
assertEquals(
|
|
||||||
originalUserState,
|
|
||||||
repository.userStateFlow.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SwitchAccountResult.NoChange,
|
SwitchAccountResult.NoChange,
|
||||||
repository.switchAccount(userId = invalidId),
|
repository.switchAccount(userId = invalidId),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
originalUserState,
|
|
||||||
repository.userStateFlow.value,
|
|
||||||
)
|
|
||||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6094,24 +5961,8 @@ class AuthRepositoryTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `switchAccount when the userId is valid should update the current UserState and reset any pending account additions`() {
|
fun `switchAccount when the userId is valid should update the current UserState and reset any pending account additions`() {
|
||||||
val updatedUserId = USER_ID_2
|
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
|
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||||
fakeEnvironmentRepository.environment = Environment.Eu
|
fakeEnvironmentRepository.environment = Environment.Eu
|
||||||
assertEquals(
|
|
||||||
originalUserState,
|
|
||||||
repository.userStateFlow.value,
|
|
||||||
)
|
|
||||||
repository.hasPendingAccountAddition = true
|
repository.hasPendingAccountAddition = true
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -6119,12 +5970,10 @@ class AuthRepositoryTest {
|
|||||||
repository.switchAccount(userId = updatedUserId),
|
repository.switchAccount(userId = updatedUserId),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
originalUserState.copy(activeUserId = updatedUserId),
|
|
||||||
repository.userStateFlow.value,
|
|
||||||
)
|
|
||||||
assertFalse(repository.hasPendingAccountAddition)
|
|
||||||
assertEquals(Environment.Eu, fakeEnvironmentRepository.environment)
|
assertEquals(Environment.Eu, fakeEnvironmentRepository.environment)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -6888,6 +6737,9 @@ class AuthRepositoryTest {
|
|||||||
)
|
)
|
||||||
// This should only be set after they complete a registration and not based on login.
|
// This should only be set after they complete a registration and not based on login.
|
||||||
assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1))
|
assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1))
|
||||||
|
verify(exactly = 1) {
|
||||||
|
userStateManager.hasPendingAccountAddition = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -6956,7 +6808,10 @@ class AuthRepositoryTest {
|
|||||||
userId = USER_ID_1,
|
userId = USER_ID_1,
|
||||||
passwordHash = PASSWORD_HASH,
|
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))
|
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 {
|
companion object {
|
||||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||||
Instant.parse("2023-10-27T12:00:00Z"),
|
Instant.parse("2023-10-27T12:00:00Z"),
|
||||||
@ -7259,18 +7078,6 @@ class AuthRepositoryTest {
|
|||||||
accessToken = ACCESS_TOKEN_2,
|
accessToken = ACCESS_TOKEN_2,
|
||||||
refreshToken = "refreshToken",
|
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(
|
private val VAULT_UNLOCK_DATA = listOf(
|
||||||
VaultUnlockData(
|
VaultUnlockData(
|
||||||
userId = USER_ID_1,
|
userId = USER_ID_1,
|
||||||
@ -7278,10 +7085,6 @@ class AuthRepositoryTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val FIRST_TIME_STATE = FirstTimeState(
|
|
||||||
showImportLoginsCard = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val SERVER_CONFIG_DEFAULT = ServerConfig(
|
private val SERVER_CONFIG_DEFAULT = ServerConfig(
|
||||||
lastSync = 0L,
|
lastSync = 0L,
|
||||||
serverData = ConfigResponseJson(
|
serverData = ConfigResponseJson(
|
||||||
|
|||||||
@ -186,7 +186,7 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `AccountDeletionConfirm should clear dialog state and call clearPendingAccountDeletion`() =
|
fun `AccountDeletionConfirm should clear dialog state and call clearPendingAccountDeletion`() =
|
||||||
runTest {
|
runTest {
|
||||||
every { authRepo.clearPendingAccountDeletion() } just runs
|
every { authRepo.hasPendingAccountDeletion = false } just runs
|
||||||
val state = DEFAULT_STATE.copy(
|
val state = DEFAULT_STATE.copy(
|
||||||
dialog = DeleteAccountState.DeleteAccountDialog.DeleteSuccess,
|
dialog = DeleteAccountState.DeleteAccountDialog.DeleteSuccess,
|
||||||
)
|
)
|
||||||
@ -198,7 +198,7 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
|||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
verify {
|
verify {
|
||||||
authRepo.clearPendingAccountDeletion()
|
authRepo.hasPendingAccountDeletion = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class DeleteAccountConfirmationViewModelTest : BaseViewModelTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `DeleteAccountAcknowledge should clear dialog and call clearPendingAccountDeletion`() =
|
fun `DeleteAccountAcknowledge should clear dialog and call clearPendingAccountDeletion`() =
|
||||||
runTest {
|
runTest {
|
||||||
every { authRepo.clearPendingAccountDeletion() } just runs
|
every { authRepo.hasPendingAccountDeletion = false } just runs
|
||||||
val state = DEFAULT_STATE.copy(
|
val state = DEFAULT_STATE.copy(
|
||||||
dialog =
|
dialog =
|
||||||
DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess(),
|
DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess(),
|
||||||
@ -68,7 +68,7 @@ class DeleteAccountConfirmationViewModelTest : BaseViewModelTest() {
|
|||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
verify {
|
verify {
|
||||||
authRepo.clearPendingAccountDeletion()
|
authRepo.hasPendingAccountDeletion = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user