mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
Refactor Vault Sync Logic into VaultSyncManager (#5871)
This commit is contained in:
parent
cfd0a5b8a5
commit
f954b0b941
@ -0,0 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that the security stamps for the current user do not match.
|
||||
*/
|
||||
class SecurityStampMismatchException : IllegalStateException("Security stamps do not match!")
|
||||
@ -0,0 +1,29 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
|
||||
/**
|
||||
* Manages the synchronization of the user's vault data with the remote server.
|
||||
* This interface provides a way to trigger a sync process, which updates the local
|
||||
* database with the latest changes from the server.
|
||||
*/
|
||||
interface VaultSyncManager {
|
||||
/**
|
||||
* Initiates a synchronization process for the user's vault data.
|
||||
*
|
||||
* This function fetches the latest data from the remote server and updates the local
|
||||
* vault cache. It can be a standard sync or a "forced" sync, which typically
|
||||
* bypasses local cache checks and fetches everything anew.
|
||||
*
|
||||
* @param userId The unique identifier of the user whose vault is to be synchronized.
|
||||
* @param forced If true, performs a full, forced synchronization, ignoring any recent sync
|
||||
* timestamps. If false, performs a standard incremental sync.
|
||||
*
|
||||
* @return A [SyncVaultDataResult] indicating the outcome of the synchronization, such as
|
||||
* success or failure with details.
|
||||
*/
|
||||
suspend fun sync(
|
||||
userId: String,
|
||||
forced: Boolean,
|
||||
): SyncVaultDataResult
|
||||
}
|
||||
@ -0,0 +1,176 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.SecurityStampMismatchException
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* Default implementation of [VaultSyncManager].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class VaultSyncManagerImpl(
|
||||
private val syncService: SyncService,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val clock: Clock,
|
||||
) : VaultSyncManager {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun sync(
|
||||
userId: String,
|
||||
forced: Boolean,
|
||||
): SyncVaultDataResult {
|
||||
if (!forced) {
|
||||
// Skip this check if we are forcing the request.
|
||||
val lastSyncInstant = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.toEpochMilli()
|
||||
lastSyncInstant?.let { lastSyncTimeMs ->
|
||||
// If the lasSyncState is null we just sync, no checks required.
|
||||
syncService
|
||||
.getAccountRevisionDateMillis()
|
||||
.fold(
|
||||
onSuccess = { serverRevisionDate ->
|
||||
if (serverRevisionDate < lastSyncTimeMs) {
|
||||
// We can skip the actual sync call if there is no new data or
|
||||
// database scheme changes since the last sync.
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.resyncVaultData(userId = userId)
|
||||
val itemsAvailable = vaultDiskSource
|
||||
.getCiphersFlow(userId)
|
||||
.firstOrNull()
|
||||
?.isNotEmpty() == true
|
||||
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
return SyncVaultDataResult.Error(throwable = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return syncService
|
||||
.sync()
|
||||
.fold(
|
||||
onSuccess = { syncResponse ->
|
||||
val localSecurityStamp = authDiskSource.userState?.activeAccount?.profile?.stamp
|
||||
val serverSecurityStamp = syncResponse.profile.securityStamp
|
||||
|
||||
// Log the user out if the stamps do not match
|
||||
localSecurityStamp?.let {
|
||||
if (serverSecurityStamp != localSecurityStamp) {
|
||||
userLogoutManager.softLogout(
|
||||
// Ensure UserLogoutManager is available
|
||||
userId = userId,
|
||||
reason = LogoutReason.SecurityStamp,
|
||||
)
|
||||
return SyncVaultDataResult.Error(
|
||||
throwable = SecurityStampMismatchException(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user information with additional information from sync response
|
||||
authDiskSource.userState = authDiskSource.userState?.toUpdatedUserStateJson(
|
||||
syncResponse = syncResponse,
|
||||
)
|
||||
|
||||
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
|
||||
storeProfileData(syncResponse = syncResponse)
|
||||
|
||||
// Treat absent network policies as known empty data to
|
||||
// distinguish between unknown null data.
|
||||
authDiskSource.storePolicies(
|
||||
userId = userId,
|
||||
policies = syncResponse.policies.orEmpty(),
|
||||
)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
|
||||
val itemsAvailable = syncResponse.ciphers?.isNotEmpty() == true
|
||||
SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
SyncVaultDataResult.Error(throwable = throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun unlockVaultForOrganizationsIfNecessary(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val profile = syncResponse.profile
|
||||
val organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: return
|
||||
|
||||
// There shouldn't be issues when unlocking directly from the syncResponse so we can ignore
|
||||
// the return type here.
|
||||
vaultSdkSource
|
||||
.initializeOrganizationCrypto(
|
||||
userId = syncResponse.profile.id,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = organizationKeys,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun storeProfileData(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val profile = syncResponse.profile
|
||||
val userId = profile.id
|
||||
authDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = userId,
|
||||
userKey = profile.key,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = profile.privateKey,
|
||||
)
|
||||
storeAccountKeys(
|
||||
userId = userId,
|
||||
accountKeys = profile.accountKeys,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = userId,
|
||||
organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) },
|
||||
)
|
||||
storeShouldUseKeyConnector(
|
||||
userId = userId,
|
||||
shouldUseKeyConnector = profile.shouldUseKeyConnector,
|
||||
)
|
||||
storeOrganizations(
|
||||
userId = userId,
|
||||
organizations = profile.organizations,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,12 @@ import com.bitwarden.core.data.manager.realtime.RealtimeManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@ -22,6 +24,8 @@ import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManagerImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@ -110,4 +114,24 @@ object VaultManagerModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
clock = clock,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVaultSyncManager(
|
||||
syncService: SyncService,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
clock: Clock,
|
||||
): VaultSyncManager = VaultSyncManagerImpl(
|
||||
syncService = syncService,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
userLogoutManager = userLogoutManager,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
package com.x8bit.bitwarden.data.vault.manager.model
|
||||
|
||||
/**
|
||||
* Represents the result of a sync operation.
|
||||
@ -14,7 +14,7 @@ sealed class SyncVaultDataResult {
|
||||
/**
|
||||
* Indicates a failed sync operation.
|
||||
*
|
||||
* @property throwable The exception that caused the failure, if any.
|
||||
* @property throwable The exception that caused the failure.
|
||||
*/
|
||||
data class Error(val throwable: Throwable?) : SyncVaultDataResult()
|
||||
data class Error(val throwable: Throwable) : SyncVaultDataResult()
|
||||
}
|
||||
@ -16,6 +16,7 @@ import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
@ -27,7 +28,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
|
||||
@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.repository
|
||||
import android.net.Uri
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
@ -19,13 +18,11 @@ import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.network.model.CreateFileSendResponse
|
||||
import com.bitwarden.network.model.CreateSendJsonResponse
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.UpdateFolderResponseJson
|
||||
import com.bitwarden.network.model.UpdateSendResponseJson
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.bitwarden.network.util.isNoConnectionError
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.Send
|
||||
@ -38,10 +35,7 @@ import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
@ -64,7 +58,9 @@ import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
@ -76,7 +72,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
@ -140,7 +135,6 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
class VaultRepositoryImpl(
|
||||
private val syncService: SyncService,
|
||||
private val ciphersService: CiphersService,
|
||||
private val sendsService: SendsService,
|
||||
private val folderService: FolderService,
|
||||
@ -152,12 +146,12 @@ class VaultRepositoryImpl(
|
||||
private val fileManager: FileManager,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val totpCodeManager: TotpCodeManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
pushManager: PushManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
private val vaultSyncManager: VaultSyncManager,
|
||||
) : VaultRepository,
|
||||
CipherManager by cipherManager,
|
||||
VaultLockManager by vaultLockManager {
|
||||
@ -378,12 +372,13 @@ class VaultRepositoryImpl(
|
||||
if (lastSyncInstant == null ||
|
||||
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES))
|
||||
) {
|
||||
sync()
|
||||
sync(forced = false)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncForResult(): SyncVaultDataResult {
|
||||
val userId = activeUserId ?: return SyncVaultDataResult.Error(throwable = null)
|
||||
val userId = activeUserId
|
||||
?: return SyncVaultDataResult.Error(throwable = NoActiveUserException())
|
||||
syncJob = ioScope
|
||||
.async { syncInternal(userId = userId, forced = false) }
|
||||
.also {
|
||||
@ -1036,42 +1031,6 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeProfileData(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val profile = syncResponse.profile
|
||||
val userId = profile.id
|
||||
authDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = userId,
|
||||
userKey = profile.key,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = profile.privateKey,
|
||||
)
|
||||
storeAccountKeys(
|
||||
userId = userId,
|
||||
accountKeys = profile.accountKeys,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = userId,
|
||||
organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) },
|
||||
)
|
||||
storeShouldUseKeyConnector(
|
||||
userId = userId,
|
||||
shouldUseKeyConnector = profile.shouldUseKeyConnector,
|
||||
)
|
||||
storeOrganizations(
|
||||
userId = userId,
|
||||
organizations = profile.organizations,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlockVaultForUser(
|
||||
userId: String,
|
||||
initUserCryptoMethod: InitUserCryptoMethod,
|
||||
@ -1102,28 +1061,6 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun unlockVaultForOrganizationsIfNecessary(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val profile = syncResponse.profile
|
||||
val organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: return
|
||||
|
||||
// There shouldn't be issues when unlocking directly from the syncResponse so we can ignore
|
||||
// the return type here.
|
||||
vaultSdkSource
|
||||
.initializeOrganizationCrypto(
|
||||
userId = syncResponse.profile.id,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = organizationKeys,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeVaultDiskCiphersToCipherListView(
|
||||
userId: String,
|
||||
): Flow<DataState<DecryptCipherListResult>> =
|
||||
@ -1500,90 +1437,17 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
//endregion Push Notification helpers
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private suspend fun syncInternal(
|
||||
userId: String,
|
||||
forced: Boolean,
|
||||
): SyncVaultDataResult {
|
||||
if (!forced) {
|
||||
// Skip this check if we are forcing the request.
|
||||
val lastSyncInstant = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.toEpochMilli()
|
||||
lastSyncInstant?.let { lastSyncTimeMs ->
|
||||
// If the lasSyncState is null we just sync, no checks required.
|
||||
syncService
|
||||
.getAccountRevisionDateMillis()
|
||||
.fold(
|
||||
onSuccess = { serverRevisionDate ->
|
||||
if (serverRevisionDate < lastSyncTimeMs) {
|
||||
// We can skip the actual sync call if there is no new data or
|
||||
// database scheme changes since the last sync.
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.resyncVaultData(userId = userId)
|
||||
val itemsAvailable = vaultDiskSource
|
||||
.getCiphersFlow(userId)
|
||||
.firstOrNull()
|
||||
?.isNotEmpty() == true
|
||||
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
updateVaultStateFlowsToError(throwable = it)
|
||||
return SyncVaultDataResult.Error(throwable = it)
|
||||
},
|
||||
)
|
||||
): SyncVaultDataResult =
|
||||
vaultSyncManager
|
||||
.sync(userId = userId, forced = forced)
|
||||
.also { result ->
|
||||
if (result is SyncVaultDataResult.Error) {
|
||||
updateVaultStateFlowsToError(throwable = result.throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return syncService
|
||||
.sync()
|
||||
.fold(
|
||||
onSuccess = { syncResponse ->
|
||||
val localSecurityStamp = authDiskSource.userState?.activeAccount?.profile?.stamp
|
||||
val serverSecurityStamp = syncResponse.profile.securityStamp
|
||||
|
||||
// Log the user out if the stamps do not match
|
||||
localSecurityStamp?.let {
|
||||
if (serverSecurityStamp != localSecurityStamp) {
|
||||
userLogoutManager.softLogout(
|
||||
userId = userId,
|
||||
reason = LogoutReason.SecurityStamp,
|
||||
)
|
||||
return SyncVaultDataResult.Error(throwable = null)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user information with additional information from sync response
|
||||
authDiskSource.userState = authDiskSource.userState?.toUpdatedUserStateJson(
|
||||
syncResponse = syncResponse,
|
||||
)
|
||||
|
||||
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
|
||||
storeProfileData(syncResponse = syncResponse)
|
||||
// Treat absent network policies as known empty data to
|
||||
// distinguish between unknown null data.
|
||||
authDiskSource.storePolicies(
|
||||
userId = userId,
|
||||
policies = syncResponse.policies.orEmpty(),
|
||||
)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
|
||||
val itemsAvailable = syncResponse.ciphers?.isNotEmpty() == true
|
||||
SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
updateVaultStateFlowsToError(throwable = throwable)
|
||||
SyncVaultDataResult.Error(throwable = throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Throwable.toNetworkOrErrorState(data: T?): DataState<T> =
|
||||
|
||||
@ -4,9 +4,7 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@ -17,6 +15,7 @@ import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl
|
||||
import dagger.Module
|
||||
@ -36,7 +35,6 @@ object VaultRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVaultRepository(
|
||||
syncService: SyncService,
|
||||
sendsService: SendsService,
|
||||
ciphersService: CiphersService,
|
||||
folderService: FolderService,
|
||||
@ -50,12 +48,11 @@ object VaultRepositoryModule {
|
||||
dispatcherManager: DispatcherManager,
|
||||
totpCodeManager: TotpCodeManager,
|
||||
pushManager: PushManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
sendsService = sendsService,
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
@ -69,9 +66,9 @@ object VaultRepositoryModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
totpCodeManager = totpCodeManager,
|
||||
pushManager = pushManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
vaultSyncManager = vaultSyncManager,
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.toUriOrNull
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
||||
@ -0,0 +1,291 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.createMockCipher
|
||||
import com.bitwarden.network.model.createMockOrganization
|
||||
import com.bitwarden.network.model.createMockOrganizationKeys
|
||||
import com.bitwarden.network.model.createMockPolicy
|
||||
import com.bitwarden.network.model.createMockProfile
|
||||
import com.bitwarden.network.model.createMockSyncResponse
|
||||
import com.bitwarden.network.service.SyncService
|
||||
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.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
class VaultSyncManagerTest {
|
||||
|
||||
private val clock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
private val syncService: SyncService = mockk {
|
||||
coEvery {
|
||||
getAccountRevisionDateMillis()
|
||||
} returns clock.instant().plus(1, ChronoUnit.MINUTES).toEpochMilli().asSuccess()
|
||||
}
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val settingsDiskSource = mockk<SettingsDiskSource> {
|
||||
every { getLastSyncTime(userId = any()) } returns clock.instant()
|
||||
every { storeLastSyncTime(userId = any(), lastSyncTime = any()) } just runs
|
||||
}
|
||||
private val mutableGetCiphersFlow: MutableStateFlow<List<SyncResponseJson.Cipher>> =
|
||||
MutableStateFlow(listOf(createMockCipher(1)))
|
||||
private val vaultDiskSource: VaultDiskSource = mockk {
|
||||
coEvery { resyncVaultData(any()) } just runs
|
||||
every { getCiphersFlow(any()) } returns mutableGetCiphersFlow
|
||||
}
|
||||
private val vaultSdkSource: VaultSdkSource = mockk {
|
||||
every { clearCrypto(userId = any()) } just runs
|
||||
}
|
||||
private val userLogoutManager: UserLogoutManager = mockk {
|
||||
every { softLogout(any(), any()) } just runs
|
||||
}
|
||||
private val vaultSyncManager = VaultSyncManagerImpl(
|
||||
syncService = syncService,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
userLogoutManager = userLogoutManager,
|
||||
clock = clock,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `sync with forced should skip checks and call the syncService sync`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns Throwable("failure").asFailure()
|
||||
|
||||
vaultSyncManager.sync(userId = "mockId-1", forced = true)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
syncService.getAccountRevisionDateMillis()
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
|
||||
} just runs
|
||||
|
||||
vaultSyncManager.sync(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
forced = false,
|
||||
)
|
||||
|
||||
val updatedUserState = MOCK_USER_STATE
|
||||
.copy(
|
||||
accounts = mapOf(
|
||||
"mockId-1" to MOCK_ACCOUNT.copy(
|
||||
profile = MOCK_PROFILE.copy(
|
||||
avatarColorHex = "mockAvatarColor-1",
|
||||
stamp = "mockSecurityStamp-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
fakeAuthDiskSource.assertUserState(
|
||||
userState = updatedUserState,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = "mockId-1",
|
||||
userKey = "mockKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = "mockId-1",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = "mockId-1",
|
||||
organizationKeys = mapOf("mockId-1" to "mockKey-1"),
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizations(
|
||||
userId = "mockId-1",
|
||||
organizations = listOf(createMockOrganization(number = 1)),
|
||||
)
|
||||
fakeAuthDiskSource.assertPolicies(
|
||||
userId = "mockId-1",
|
||||
policies = listOf(createMockPolicy(number = 1)),
|
||||
)
|
||||
fakeAuthDiskSource.assertShouldUseKeyConnector(
|
||||
userId = "mockId-1",
|
||||
shouldUseKeyConnector = false,
|
||||
)
|
||||
coVerify {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `sync with syncService Success with a different security stamp should logout and return early`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse
|
||||
.copy(profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"))
|
||||
.asSuccess()
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
|
||||
vaultSyncManager.sync(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
forced = false,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp)
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = any(),
|
||||
)
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync should return error when getAccountRevisionDateMillis fails`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val throwable = Throwable()
|
||||
coEvery {
|
||||
syncService.getAccountRevisionDateMillis()
|
||||
} returns throwable.asFailure()
|
||||
val syncResult = vaultSyncManager.sync(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
forced = false,
|
||||
)
|
||||
assertEquals(
|
||||
SyncVaultDataResult.Error(throwable = throwable),
|
||||
syncResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync when the last sync time is more recent than the revision date should not sync `() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().plus(2, ChronoUnit.MINUTES)
|
||||
|
||||
vaultSyncManager.sync(
|
||||
userId = userId,
|
||||
forced = false,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
}
|
||||
}
|
||||
|
||||
private val MOCK_PROFILE = AccountJson.Profile(
|
||||
userId = "mockId-1",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = null,
|
||||
stamp = "mockSecurityStamp-1",
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremium = false,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = null,
|
||||
kdfIterations = null,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
|
||||
private val MOCK_ACCOUNT = AccountJson(
|
||||
profile = MOCK_PROFILE,
|
||||
tokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val MOCK_USER_STATE = UserStateJson(
|
||||
activeUserId = "mockId-1",
|
||||
accounts = mapOf(
|
||||
"mockId-1" to MOCK_ACCOUNT,
|
||||
),
|
||||
)
|
||||
@ -6,7 +6,6 @@ import app.cash.turbine.test
|
||||
import app.cash.turbine.turbineScope
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
@ -29,17 +28,12 @@ import com.bitwarden.network.model.createMockCollection
|
||||
import com.bitwarden.network.model.createMockDomains
|
||||
import com.bitwarden.network.model.createMockFileSendResponseJson
|
||||
import com.bitwarden.network.model.createMockFolder
|
||||
import com.bitwarden.network.model.createMockOrganization
|
||||
import com.bitwarden.network.model.createMockOrganizationKeys
|
||||
import com.bitwarden.network.model.createMockPolicy
|
||||
import com.bitwarden.network.model.createMockProfile
|
||||
import com.bitwarden.network.model.createMockSend
|
||||
import com.bitwarden.network.model.createMockSendJsonRequest
|
||||
import com.bitwarden.network.model.createMockSyncResponse
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
@ -53,8 +47,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.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
@ -70,7 +62,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAccount
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
@ -86,6 +77,8 @@ import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
@ -97,7 +90,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
@ -156,21 +148,12 @@ class VaultRepositoryTest {
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
private val userLogoutManager: UserLogoutManager = mockk {
|
||||
every { softLogout(any(), any()) } just runs
|
||||
}
|
||||
private val fileManager: FileManager = mockk {
|
||||
coEvery { delete(*anyVararg()) } just runs
|
||||
}
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val settingsDiskSource = mockk<SettingsDiskSource> {
|
||||
every { getLastSyncTime(userId = any()) } returns clock.instant()
|
||||
every { storeLastSyncTime(userId = any(), lastSyncTime = any()) } just runs
|
||||
}
|
||||
private val syncService: SyncService = mockk {
|
||||
coEvery {
|
||||
getAccountRevisionDateMillis()
|
||||
} returns clock.instant().plus(1, ChronoUnit.MINUTES).toEpochMilli().asSuccess()
|
||||
}
|
||||
private val sendsService: SendsService = mockk()
|
||||
private val ciphersService: CiphersService = mockk()
|
||||
@ -232,9 +215,9 @@ class VaultRepositoryTest {
|
||||
every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow
|
||||
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
|
||||
}
|
||||
private val vaultSyncManager: VaultSyncManager = mockk()
|
||||
|
||||
private val vaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
sendsService = sendsService,
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
@ -249,9 +232,9 @@ class VaultRepositoryTest {
|
||||
cipherManager = cipherManager,
|
||||
fileManager = fileManager,
|
||||
clock = clock,
|
||||
userLogoutManager = userLogoutManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
vaultSyncManager = vaultSyncManager,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
@ -283,13 +266,13 @@ class VaultRepositoryTest {
|
||||
@Test
|
||||
fun `userSwitchingChangesFlow should cancel any pending sync call`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(any(), any()) } just awaits
|
||||
|
||||
vaultRepository.sync()
|
||||
vaultRepository.sync()
|
||||
coVerify(exactly = 1) {
|
||||
// Despite being called twice, we only allow 1 sync
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
|
||||
fakeAuthDiskSource.userState = UserStateJson(
|
||||
@ -297,10 +280,10 @@ class VaultRepositoryTest {
|
||||
accounts = mapOf("mockId-2" to mockk()),
|
||||
)
|
||||
vaultRepository.sync()
|
||||
coVerify(exactly = 2) {
|
||||
coVerify {
|
||||
// A second sync should have happened now since it was cancelled by the userState change
|
||||
syncService.getAccountRevisionDateMillis()
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(userId = "mockId-1", forced = any())
|
||||
vaultSyncManager.sync(userId = "mockId-2", forced = any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -815,227 +798,108 @@ class VaultRepositoryTest {
|
||||
@Test
|
||||
fun `databaseSchemeChangeFlow should trigger sync on emission`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(any(), any()) } just awaits
|
||||
|
||||
mutableDatabaseSchemeChangeFlow.tryEmit(Unit)
|
||||
|
||||
coVerify(exactly = 1) { syncService.sync() }
|
||||
coVerify(exactly = 1) { vaultSyncManager.sync(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with forced should skip checks and call the syncService sync`() {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns Throwable("failure").asFailure()
|
||||
|
||||
vaultRepository.sync(forced = true)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
syncService.getAccountRevisionDateMillis()
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() =
|
||||
fun `sync should update DataStateFlow with an Error when vaultSyncManager result is Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
|
||||
val mockException = IllegalStateException("sad")
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
vaultSyncManager.sync(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
forced = false,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
|
||||
} just runs
|
||||
} returns SyncVaultDataResult.Error(throwable = mockException)
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
val updatedUserState = MOCK_USER_STATE
|
||||
.copy(
|
||||
accounts = mapOf(
|
||||
"mockId-1" to MOCK_ACCOUNT.copy(
|
||||
profile = MOCK_PROFILE.copy(
|
||||
avatarColorHex = "mockAvatarColor-1",
|
||||
stamp = "mockSecurityStamp-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
fakeAuthDiskSource.assertUserState(
|
||||
userState = updatedUserState,
|
||||
assertEquals(
|
||||
DataState.Error<DecryptCipherListResult>(mockException),
|
||||
vaultRepository.decryptCipherListResultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = "mockId-1",
|
||||
userKey = "mockKey-1",
|
||||
assertEquals(
|
||||
DataState.Error<List<CollectionView>>(mockException),
|
||||
vaultRepository.collectionsStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = "mockId-1",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
assertEquals(
|
||||
DataState.Error<DomainsData>(mockException),
|
||||
vaultRepository.domainsStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = "mockId-1",
|
||||
organizationKeys = mapOf("mockId-1" to "mockKey-1"),
|
||||
assertEquals(
|
||||
DataState.Error<List<FolderView>>(mockException),
|
||||
vaultRepository.foldersStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizations(
|
||||
userId = "mockId-1",
|
||||
organizations = listOf(createMockOrganization(number = 1)),
|
||||
assertEquals(
|
||||
DataState.Error<SendData>(mockException),
|
||||
vaultRepository.sendDataStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertPolicies(
|
||||
userId = "mockId-1",
|
||||
policies = listOf(createMockPolicy(number = 1)),
|
||||
)
|
||||
fakeAuthDiskSource.assertShouldUseKeyConnector(
|
||||
userId = "mockId-1",
|
||||
shouldUseKeyConnector = false,
|
||||
)
|
||||
coVerify {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `sync with syncService Success with a different security stamp should logout and return early`() =
|
||||
fun `sync should update vaultDataStateFlow with an Error when vaultSyncManager result is Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse
|
||||
.copy(profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"))
|
||||
.asSuccess()
|
||||
|
||||
val mockException = IllegalStateException("sad")
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(mockException)
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.test {
|
||||
assertEquals(DataState.Loading, awaitItem())
|
||||
vaultRepository.sync()
|
||||
assertEquals(DataState.Error<VaultData>(mockException), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `sync should update DataStateFlows to NoNetwork when vaultSyncManager result is Error with `() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(throwable = UnknownHostException())
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp)
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = any(),
|
||||
)
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.decryptCipherListResultStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.collectionsStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.domainsStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.foldersStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.sendDataStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with syncService Failure should update DataStateFlow with an Error`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val mockException = IllegalStateException("sad")
|
||||
coEvery { syncService.sync() } returns mockException.asFailure()
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
assertEquals(
|
||||
DataState.Error<DecryptCipherListResult>(mockException),
|
||||
vaultRepository.decryptCipherListResultStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.Error<List<CollectionView>>(mockException),
|
||||
vaultRepository.collectionsStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.Error<DomainsData>(mockException),
|
||||
vaultRepository.domainsStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.Error<List<FolderView>>(mockException),
|
||||
vaultRepository.foldersStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.Error<SendData>(mockException),
|
||||
vaultRepository.sendDataStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with syncService Failure should update vaultDataStateFlow with an Error`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val mockException = IllegalStateException("sad")
|
||||
coEvery { syncService.sync() } returns mockException.asFailure()
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.test {
|
||||
assertEquals(DataState.Loading, awaitItem())
|
||||
vaultRepository.sync()
|
||||
assertEquals(DataState.Error<VaultData>(mockException), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with NoNetwork should update DataStateFlows to NoNetwork`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.decryptCipherListResultStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.collectionsStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.domainsStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.foldersStateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
DataState.NoNetwork(data = null),
|
||||
vaultRepository.sendDataStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with NoNetwork should update vaultDataStateFlow to NoNetwork`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(UnknownHostException())
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository
|
||||
@ -1054,7 +918,12 @@ class VaultRepositoryTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
setVaultToUnlocked(userId = userId)
|
||||
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(
|
||||
userId = userId,
|
||||
forced = false,
|
||||
)
|
||||
} returns SyncVaultDataResult.Error(throwable = UnknownHostException())
|
||||
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
|
||||
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
|
||||
coEvery {
|
||||
@ -1092,11 +961,11 @@ class VaultRepositoryTest {
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns null
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
|
||||
|
||||
vaultRepository.syncIfNecessary()
|
||||
|
||||
coVerify { syncService.sync() }
|
||||
coVerify { vaultSyncManager.sync(userId = userId, forced = false) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@ -1107,11 +976,11 @@ class VaultRepositoryTest {
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().minus(31, ChronoUnit.MINUTES)
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
|
||||
|
||||
vaultRepository.syncIfNecessary()
|
||||
|
||||
coVerify { syncService.sync() }
|
||||
coVerify { vaultSyncManager.sync(userId = userId, forced = false) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@ -1122,11 +991,11 @@ class VaultRepositoryTest {
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().minus(29, ChronoUnit.MINUTES)
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
|
||||
|
||||
vaultRepository.syncIfNecessary()
|
||||
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
coVerify(exactly = 0) { vaultSyncManager.sync(userId = any(), forced = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1137,33 +1006,13 @@ class VaultRepositoryTest {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().minus(1, ChronoUnit.MINUTES)
|
||||
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
coVerify { syncService.sync() }
|
||||
coVerify { vaultSyncManager.sync(userId = userId, forced = false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync when the last sync time is more recent than the revision date should not sync `() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().plus(2, ChronoUnit.MINUTES)
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
verify(exactly = 1) {
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lockVaultForCurrentUser should delegate to the VaultLockManager`() {
|
||||
vaultRepository.lockVaultForCurrentUser(isUserInitiated = true)
|
||||
@ -1946,7 +1795,9 @@ class VaultRepositoryTest {
|
||||
val folderIdString = "mockId-$folderId"
|
||||
val throwable = Throwable("Fail")
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns throwable.asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(throwable)
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository.getVaultItemStateFlow(folderIdString).test {
|
||||
@ -1956,7 +1807,7 @@ class VaultRepositoryTest {
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1966,7 +1817,9 @@ class VaultRepositoryTest {
|
||||
val itemId = 1234
|
||||
val itemIdString = "mockId-$itemId"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(UnknownHostException())
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository.getVaultItemStateFlow(itemIdString).test {
|
||||
@ -1976,7 +1829,7 @@ class VaultRepositoryTest {
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2093,7 +1946,9 @@ class VaultRepositoryTest {
|
||||
val folderId = 1234
|
||||
val folderIdString = "mockId-$folderId"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(UnknownHostException())
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository.getVaultFolderStateFlow(folderIdString).test {
|
||||
@ -2103,7 +1958,7 @@ class VaultRepositoryTest {
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2114,7 +1969,9 @@ class VaultRepositoryTest {
|
||||
val folderIdString = "mockId-$folderId"
|
||||
val throwable = Throwable("Fail")
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns throwable.asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(throwable)
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository.getVaultFolderStateFlow(folderIdString).test {
|
||||
@ -2124,7 +1981,7 @@ class VaultRepositoryTest {
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2169,7 +2026,9 @@ class VaultRepositoryTest {
|
||||
runTest {
|
||||
val sendId = 1234
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(UnknownHostException())
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository.getSendStateFlow("mockId-$sendId").test {
|
||||
@ -2179,7 +2038,7 @@ class VaultRepositoryTest {
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2189,7 +2048,9 @@ class VaultRepositoryTest {
|
||||
val sendId = 1234
|
||||
val throwable = Throwable("Fail")
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } returns throwable.asFailure()
|
||||
coEvery {
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(throwable)
|
||||
setupVaultDiskSourceFlows()
|
||||
|
||||
vaultRepository.getSendStateFlow("mockId-$sendId").test {
|
||||
@ -2199,7 +2060,7 @@ class VaultRepositoryTest {
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
syncService.sync()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@ -3175,29 +3036,9 @@ class VaultRepositoryTest {
|
||||
val userId = "mockId-1"
|
||||
setVaultToUnlocked(userId = userId)
|
||||
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
clock.instant(),
|
||||
)
|
||||
} just runs
|
||||
vaultSyncManager.sync(userId = userId, forced = false)
|
||||
} returns SyncVaultDataResult.Success(itemsAvailable = true)
|
||||
|
||||
val stateFlow = MutableStateFlow<DataState<VerificationCodeItem?>>(
|
||||
DataState.Loading,
|
||||
@ -3243,28 +3084,9 @@ class VaultRepositoryTest {
|
||||
val userId = "mockId-1"
|
||||
setVaultToUnlocked(userId = userId)
|
||||
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
clock.instant(),
|
||||
)
|
||||
} just runs
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Success(itemsAvailable = true)
|
||||
|
||||
val stateFlow = MutableStateFlow<DataState<List<VerificationCodeItem>>>(
|
||||
DataState.Loading,
|
||||
@ -3308,11 +3130,11 @@ class VaultRepositoryTest {
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns null
|
||||
coEvery { syncService.sync() } just awaits
|
||||
coEvery { vaultSyncManager.sync(any(), any()) } just awaits
|
||||
|
||||
mutableFullSyncFlow.tryEmit(Unit)
|
||||
|
||||
coVerify { syncService.sync() }
|
||||
coVerify { vaultSyncManager.sync(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -4393,31 +4215,12 @@ class VaultRepositoryTest {
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery {
|
||||
syncService.sync()
|
||||
} returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
vaultSyncManager.sync(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
forced = false,
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
clock.instant(),
|
||||
)
|
||||
} just runs
|
||||
} returns SyncVaultDataResult.Success(itemsAvailable = true)
|
||||
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
assertEquals(SyncVaultDataResult.Success(itemsAvailable = true), syncResult)
|
||||
@ -4429,58 +4232,21 @@ class VaultRepositoryTest {
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1).copy(ciphers = emptyList())
|
||||
coEvery {
|
||||
syncService.sync()
|
||||
} returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
clock.instant(),
|
||||
)
|
||||
} just runs
|
||||
vaultSyncManager.sync(userId = userId, forced = false)
|
||||
} returns SyncVaultDataResult.Success(itemsAvailable = false)
|
||||
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
assertEquals(SyncVaultDataResult.Success(itemsAvailable = false), syncResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncForResult should return error when getAccountRevisionDateMillis fails`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val throwable = Throwable()
|
||||
coEvery {
|
||||
syncService.getAccountRevisionDateMillis()
|
||||
} returns throwable.asFailure()
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
assertEquals(
|
||||
SyncVaultDataResult.Error(throwable = throwable),
|
||||
syncResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncForResult should return error when sync fails`() = runTest {
|
||||
fun `syncForResult should return error when VaultSyncManager sync result is Error`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val throwable = Throwable()
|
||||
coEvery {
|
||||
syncService.sync()
|
||||
} returns throwable.asFailure()
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Error(throwable)
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
assertEquals(
|
||||
SyncVaultDataResult.Error(throwable = throwable),
|
||||
@ -4488,30 +4254,6 @@ class VaultRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncForResult when the last sync time is more recent than the revision date should return result from disk source data`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().plus(2, ChronoUnit.MINUTES)
|
||||
mutableGetCiphersFlow.update { emptyList() }
|
||||
val result = vaultRepository.syncForResult()
|
||||
assertEquals(
|
||||
SyncVaultDataResult.Success(itemsAvailable = false),
|
||||
result,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
/**
|
||||
@ -4523,22 +4265,9 @@ class VaultRepositoryTest {
|
||||
mockPin: String = "1234",
|
||||
) {
|
||||
val userId = "mockId-1"
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = userId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
vaultSyncManager.sync(any(), any())
|
||||
} returns SyncVaultDataResult.Success(itemsAvailable = true)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSendList(
|
||||
userId = userId,
|
||||
|
||||
@ -9,8 +9,8 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManagerImpl
|
||||
import io.mockk.coEvery
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user