Refactor Vault Sync Logic into VaultSyncManager (#5871)

This commit is contained in:
Patrick Honkonen 2025-09-16 12:44:52 -04:00 committed by GitHub
parent cfd0a5b8a5
commit f954b0b941
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 679 additions and 563 deletions

View File

@ -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!")

View File

@ -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
}

View File

@ -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,
)
}
}
}

View File

@ -5,10 +5,12 @@ import com.bitwarden.core.data.manager.realtime.RealtimeManager
import com.bitwarden.data.manager.DispatcherManager import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.CiphersService import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.DownloadService 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.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
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.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.AppStateManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository 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.TotpCodeManagerImpl
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl 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.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -110,4 +114,24 @@ object VaultManagerModule {
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
clock = clock, 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,
)
} }

View File

@ -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. * Represents the result of a sync operation.
@ -14,7 +14,7 @@ sealed class SyncVaultDataResult {
/** /**
* Indicates a failed sync operation. * 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()
} }

View File

@ -16,6 +16,7 @@ import com.bitwarden.vault.DecryptCipherListResult
import com.bitwarden.vault.FolderView import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager 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.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult 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.ImportCxfPayloadResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult 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.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.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult 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.UpdateSendResult

View File

@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.repository
import android.net.Uri import android.net.Uri
import com.bitwarden.collections.CollectionView import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow 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.fido.Fido2CredentialAutofillView
import com.bitwarden.network.model.CreateFileSendResponse import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateSendJsonResponse import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateFolderResponseJson import com.bitwarden.network.model.UpdateFolderResponseJson
import com.bitwarden.network.model.UpdateSendResponseJson import com.bitwarden.network.model.UpdateSendResponseJson
import com.bitwarden.network.service.CiphersService import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.FolderService import com.bitwarden.network.service.FolderService
import com.bitwarden.network.service.SendsService import com.bitwarden.network.service.SendsService
import com.bitwarden.network.service.SyncService
import com.bitwarden.network.util.isNoConnectionError import com.bitwarden.network.util.isNoConnectionError
import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.send.Send import com.bitwarden.send.Send
@ -38,10 +35,7 @@ import com.bitwarden.vault.CipherView
import com.bitwarden.vault.DecryptCipherListResult import com.bitwarden.vault.DecryptCipherListResult
import com.bitwarden.vault.FolderView import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource 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.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.autofill.util.login import com.x8bit.bitwarden.data.autofill.util.login
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager 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.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.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult 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.ImportCxfPayloadResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult 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.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.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult 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.UpdateSendResult
@ -140,7 +135,6 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
*/ */
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass") @Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
class VaultRepositoryImpl( class VaultRepositoryImpl(
private val syncService: SyncService,
private val ciphersService: CiphersService, private val ciphersService: CiphersService,
private val sendsService: SendsService, private val sendsService: SendsService,
private val folderService: FolderService, private val folderService: FolderService,
@ -152,12 +146,12 @@ class VaultRepositoryImpl(
private val fileManager: FileManager, private val fileManager: FileManager,
private val vaultLockManager: VaultLockManager, private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager, private val totpCodeManager: TotpCodeManager,
private val userLogoutManager: UserLogoutManager,
databaseSchemeManager: DatabaseSchemeManager, databaseSchemeManager: DatabaseSchemeManager,
pushManager: PushManager, pushManager: PushManager,
private val clock: Clock, private val clock: Clock,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
private val reviewPromptManager: ReviewPromptManager, private val reviewPromptManager: ReviewPromptManager,
private val vaultSyncManager: VaultSyncManager,
) : VaultRepository, ) : VaultRepository,
CipherManager by cipherManager, CipherManager by cipherManager,
VaultLockManager by vaultLockManager { VaultLockManager by vaultLockManager {
@ -378,12 +372,13 @@ class VaultRepositoryImpl(
if (lastSyncInstant == null || if (lastSyncInstant == null ||
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES)) currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES))
) { ) {
sync() sync(forced = false)
} }
} }
override suspend fun syncForResult(): SyncVaultDataResult { override suspend fun syncForResult(): SyncVaultDataResult {
val userId = activeUserId ?: return SyncVaultDataResult.Error(throwable = null) val userId = activeUserId
?: return SyncVaultDataResult.Error(throwable = NoActiveUserException())
syncJob = ioScope syncJob = ioScope
.async { syncInternal(userId = userId, forced = false) } .async { syncInternal(userId = userId, forced = false) }
.also { .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( private suspend fun unlockVaultForUser(
userId: String, userId: String,
initUserCryptoMethod: InitUserCryptoMethod, 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( private fun observeVaultDiskCiphersToCipherListView(
userId: String, userId: String,
): Flow<DataState<DecryptCipherListResult>> = ): Flow<DataState<DecryptCipherListResult>> =
@ -1500,90 +1437,17 @@ class VaultRepositoryImpl(
} }
//endregion Push Notification helpers //endregion Push Notification helpers
@Suppress("LongMethod")
private suspend fun syncInternal( private suspend fun syncInternal(
userId: String, userId: String,
forced: Boolean, forced: Boolean,
): SyncVaultDataResult { ): SyncVaultDataResult =
if (!forced) { vaultSyncManager
// Skip this check if we are forcing the request. .sync(userId = userId, forced = forced)
val lastSyncInstant = settingsDiskSource .also { result ->
.getLastSyncTime(userId = userId) if (result is SyncVaultDataResult.Error) {
?.toEpochMilli() updateVaultStateFlowsToError(throwable = result.throwable)
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)
},
)
} }
}
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> = private fun <T> Throwable.toNetworkOrErrorState(data: T?): DataState<T> =

View File

@ -4,9 +4,7 @@ import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.CiphersService import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.FolderService import com.bitwarden.network.service.FolderService
import com.bitwarden.network.service.SendsService 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.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.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.PushManager 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager 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.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl
import dagger.Module import dagger.Module
@ -36,7 +35,6 @@ object VaultRepositoryModule {
@Provides @Provides
@Singleton @Singleton
fun providesVaultRepository( fun providesVaultRepository(
syncService: SyncService,
sendsService: SendsService, sendsService: SendsService,
ciphersService: CiphersService, ciphersService: CiphersService,
folderService: FolderService, folderService: FolderService,
@ -50,12 +48,11 @@ object VaultRepositoryModule {
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
totpCodeManager: TotpCodeManager, totpCodeManager: TotpCodeManager,
pushManager: PushManager, pushManager: PushManager,
userLogoutManager: UserLogoutManager,
databaseSchemeManager: DatabaseSchemeManager, databaseSchemeManager: DatabaseSchemeManager,
clock: Clock, clock: Clock,
reviewPromptManager: ReviewPromptManager, reviewPromptManager: ReviewPromptManager,
vaultSyncManager: VaultSyncManager,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
syncService = syncService,
sendsService = sendsService, sendsService = sendsService,
ciphersService = ciphersService, ciphersService = ciphersService,
folderService = folderService, folderService = folderService,
@ -69,9 +66,9 @@ object VaultRepositoryModule {
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
pushManager = pushManager, pushManager = pushManager,
userLogoutManager = userLogoutManager,
databaseSchemeManager = databaseSchemeManager, databaseSchemeManager = databaseSchemeManager,
clock = clock, clock = clock,
reviewPromptManager = reviewPromptManager, reviewPromptManager = reviewPromptManager,
vaultSyncManager = vaultSyncManager,
) )
} }

View File

@ -9,8 +9,8 @@ import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.util.toUriOrNull 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.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.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel

View File

@ -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,
),
)

View File

@ -6,7 +6,6 @@ import app.cash.turbine.test
import app.cash.turbine.turbineScope import app.cash.turbine.turbineScope
import com.bitwarden.collections.CollectionView import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow 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.createMockDomains
import com.bitwarden.network.model.createMockFileSendResponseJson import com.bitwarden.network.model.createMockFileSendResponseJson
import com.bitwarden.network.model.createMockFolder import com.bitwarden.network.model.createMockFolder
import com.bitwarden.network.model.createMockOrganization
import com.bitwarden.network.model.createMockOrganizationKeys 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.createMockSend
import com.bitwarden.network.model.createMockSendJsonRequest import com.bitwarden.network.model.createMockSendJsonRequest
import com.bitwarden.network.model.createMockSyncResponse
import com.bitwarden.network.service.CiphersService import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.FolderService import com.bitwarden.network.service.FolderService
import com.bitwarden.network.service.SendsService import com.bitwarden.network.service.SendsService
import com.bitwarden.network.service.SyncService
import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView 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.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson 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.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.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException 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.platform.manager.model.SyncSendUpsertData
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource 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.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.createMockAccount
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager 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.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult 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.ImportCxfPayloadResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult 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.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.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
@ -156,21 +148,12 @@ class VaultRepositoryTest {
ZoneOffset.UTC, ZoneOffset.UTC,
) )
private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val userLogoutManager: UserLogoutManager = mockk {
every { softLogout(any(), any()) } just runs
}
private val fileManager: FileManager = mockk { private val fileManager: FileManager = mockk {
coEvery { delete(*anyVararg()) } just runs coEvery { delete(*anyVararg()) } just runs
} }
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
private val settingsDiskSource = mockk<SettingsDiskSource> { private val settingsDiskSource = mockk<SettingsDiskSource> {
every { getLastSyncTime(userId = any()) } returns clock.instant() 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 sendsService: SendsService = mockk()
private val ciphersService: CiphersService = mockk() private val ciphersService: CiphersService = mockk()
@ -232,9 +215,9 @@ class VaultRepositoryTest {
every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
} }
private val vaultSyncManager: VaultSyncManager = mockk()
private val vaultRepository = VaultRepositoryImpl( private val vaultRepository = VaultRepositoryImpl(
syncService = syncService,
sendsService = sendsService, sendsService = sendsService,
ciphersService = ciphersService, ciphersService = ciphersService,
folderService = folderService, folderService = folderService,
@ -249,9 +232,9 @@ class VaultRepositoryTest {
cipherManager = cipherManager, cipherManager = cipherManager,
fileManager = fileManager, fileManager = fileManager,
clock = clock, clock = clock,
userLogoutManager = userLogoutManager,
databaseSchemeManager = databaseSchemeManager, databaseSchemeManager = databaseSchemeManager,
reviewPromptManager = reviewPromptManager, reviewPromptManager = reviewPromptManager,
vaultSyncManager = vaultSyncManager,
) )
@BeforeEach @BeforeEach
@ -283,13 +266,13 @@ class VaultRepositoryTest {
@Test @Test
fun `userSwitchingChangesFlow should cancel any pending sync call`() = runTest { fun `userSwitchingChangesFlow should cancel any pending sync call`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(any(), any()) } just awaits
vaultRepository.sync() vaultRepository.sync()
vaultRepository.sync() vaultRepository.sync()
coVerify(exactly = 1) { coVerify(exactly = 1) {
// Despite being called twice, we only allow 1 sync // Despite being called twice, we only allow 1 sync
syncService.sync() vaultSyncManager.sync(any(), any())
} }
fakeAuthDiskSource.userState = UserStateJson( fakeAuthDiskSource.userState = UserStateJson(
@ -297,10 +280,10 @@ class VaultRepositoryTest {
accounts = mapOf("mockId-2" to mockk()), accounts = mapOf("mockId-2" to mockk()),
) )
vaultRepository.sync() vaultRepository.sync()
coVerify(exactly = 2) { coVerify {
// A second sync should have happened now since it was cancelled by the userState change // A second sync should have happened now since it was cancelled by the userState change
syncService.getAccountRevisionDateMillis() vaultSyncManager.sync(userId = "mockId-1", forced = any())
syncService.sync() vaultSyncManager.sync(userId = "mockId-2", forced = any())
} }
} }
@ -815,227 +798,108 @@ class VaultRepositoryTest {
@Test @Test
fun `databaseSchemeChangeFlow should trigger sync on emission`() = runTest { fun `databaseSchemeChangeFlow should trigger sync on emission`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(any(), any()) } just awaits
mutableDatabaseSchemeChangeFlow.tryEmit(Unit) mutableDatabaseSchemeChangeFlow.tryEmit(Unit)
coVerify(exactly = 1) { syncService.sync() } coVerify(exactly = 1) { vaultSyncManager.sync(any(), any()) }
} }
@Test @Test
fun `sync with forced should skip checks and call the syncService sync`() { fun `sync should update DataStateFlow with an Error when vaultSyncManager result is Error`() =
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`() =
runTest { runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1" val mockException = IllegalStateException("sad")
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery { coEvery {
vaultSdkSource.initializeOrganizationCrypto( vaultSyncManager.sync(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId, userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse, forced = false,
) )
} just runs } returns SyncVaultDataResult.Error(throwable = mockException)
every {
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
} just runs
vaultRepository.sync() vaultRepository.sync()
val updatedUserState = MOCK_USER_STATE assertEquals(
.copy( DataState.Error<DecryptCipherListResult>(mockException),
accounts = mapOf( vaultRepository.decryptCipherListResultStateFlow.value,
"mockId-1" to MOCK_ACCOUNT.copy(
profile = MOCK_PROFILE.copy(
avatarColorHex = "mockAvatarColor-1",
stamp = "mockSecurityStamp-1",
),
),
),
)
fakeAuthDiskSource.assertUserState(
userState = updatedUserState,
) )
fakeAuthDiskSource.assertUserKey( assertEquals(
userId = "mockId-1", DataState.Error<List<CollectionView>>(mockException),
userKey = "mockKey-1", vaultRepository.collectionsStateFlow.value,
) )
fakeAuthDiskSource.assertPrivateKey( assertEquals(
userId = "mockId-1", DataState.Error<DomainsData>(mockException),
privateKey = "mockPrivateKey-1", vaultRepository.domainsStateFlow.value,
) )
fakeAuthDiskSource.assertOrganizationKeys( assertEquals(
userId = "mockId-1", DataState.Error<List<FolderView>>(mockException),
organizationKeys = mapOf("mockId-1" to "mockKey-1"), vaultRepository.foldersStateFlow.value,
) )
fakeAuthDiskSource.assertOrganizations( assertEquals(
userId = "mockId-1", DataState.Error<SendData>(mockException),
organizations = listOf(createMockOrganization(number = 1)), 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") @Suppress("MaxLineLength")
@Test @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 { runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1" val mockException = IllegalStateException("sad")
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse
.copy(profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"))
.asSuccess()
coEvery { coEvery {
vaultSdkSource.initializeOrganizationCrypto( vaultSyncManager.sync(any(), any())
userId = userId, } returns SyncVaultDataResult.Error(mockException)
request = InitOrgCryptoRequest( setupVaultDiskSourceFlows()
organizationKeys = createMockOrganizationKeys(1),
), vaultRepository
) .vaultDataStateFlow
} returns InitializeCryptoResult.Success.asSuccess() .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() vaultRepository.sync()
coVerify(exactly = 1) { assertEquals(
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.SecurityStamp) DataState.NoNetwork(data = null),
} vaultRepository.decryptCipherListResultStateFlow.value,
)
coVerify(exactly = 0) { assertEquals(
vaultDiskSource.replaceVaultData( DataState.NoNetwork(data = null),
userId = MOCK_USER_STATE.activeUserId, vaultRepository.collectionsStateFlow.value,
vault = any(), )
) assertEquals(
vaultSdkSource.initializeOrganizationCrypto( DataState.NoNetwork(data = null),
userId = userId, vaultRepository.domainsStateFlow.value,
request = InitOrgCryptoRequest( )
organizationKeys = createMockOrganizationKeys(1), 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 @Test
fun `sync with NoNetwork should update vaultDataStateFlow to NoNetwork`() = runTest { fun `sync with NoNetwork should update vaultDataStateFlow to NoNetwork`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns UnknownHostException().asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(UnknownHostException())
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository vaultRepository
@ -1054,7 +918,12 @@ class VaultRepositoryTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1" val userId = "mockId-1"
setVaultToUnlocked(userId = userId) 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>>() val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
setupVaultDiskSourceFlows(sendsFlow = sendsFlow) setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
coEvery { coEvery {
@ -1092,11 +961,11 @@ class VaultRepositoryTest {
every { every {
settingsDiskSource.getLastSyncTime(userId = userId) settingsDiskSource.getLastSyncTime(userId = userId)
} returns null } returns null
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
vaultRepository.syncIfNecessary() vaultRepository.syncIfNecessary()
coVerify { syncService.sync() } coVerify { vaultSyncManager.sync(userId = userId, forced = false) }
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -1107,11 +976,11 @@ class VaultRepositoryTest {
every { every {
settingsDiskSource.getLastSyncTime(userId = userId) settingsDiskSource.getLastSyncTime(userId = userId)
} returns clock.instant().minus(31, ChronoUnit.MINUTES) } returns clock.instant().minus(31, ChronoUnit.MINUTES)
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
vaultRepository.syncIfNecessary() vaultRepository.syncIfNecessary()
coVerify { syncService.sync() } coVerify { vaultSyncManager.sync(userId = userId, forced = false) }
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -1122,11 +991,11 @@ class VaultRepositoryTest {
every { every {
settingsDiskSource.getLastSyncTime(userId = userId) settingsDiskSource.getLastSyncTime(userId = userId)
} returns clock.instant().minus(29, ChronoUnit.MINUTES) } returns clock.instant().minus(29, ChronoUnit.MINUTES)
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
vaultRepository.syncIfNecessary() vaultRepository.syncIfNecessary()
coVerify(exactly = 0) { syncService.sync() } coVerify(exactly = 0) { vaultSyncManager.sync(userId = any(), forced = any()) }
} }
@Test @Test
@ -1137,33 +1006,13 @@ class VaultRepositoryTest {
settingsDiskSource.getLastSyncTime(userId = userId) settingsDiskSource.getLastSyncTime(userId = userId)
} returns clock.instant().minus(1, ChronoUnit.MINUTES) } returns clock.instant().minus(1, ChronoUnit.MINUTES)
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(userId = userId, forced = false) } just awaits
vaultRepository.sync() 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 @Test
fun `lockVaultForCurrentUser should delegate to the VaultLockManager`() { fun `lockVaultForCurrentUser should delegate to the VaultLockManager`() {
vaultRepository.lockVaultForCurrentUser(isUserInitiated = true) vaultRepository.lockVaultForCurrentUser(isUserInitiated = true)
@ -1946,7 +1795,9 @@ class VaultRepositoryTest {
val folderIdString = "mockId-$folderId" val folderIdString = "mockId-$folderId"
val throwable = Throwable("Fail") val throwable = Throwable("Fail")
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns throwable.asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(throwable)
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository.getVaultItemStateFlow(folderIdString).test { vaultRepository.getVaultItemStateFlow(folderIdString).test {
@ -1956,7 +1807,7 @@ class VaultRepositoryTest {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
syncService.sync() vaultSyncManager.sync(any(), any())
} }
} }
@ -1966,7 +1817,9 @@ class VaultRepositoryTest {
val itemId = 1234 val itemId = 1234
val itemIdString = "mockId-$itemId" val itemIdString = "mockId-$itemId"
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns UnknownHostException().asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(UnknownHostException())
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository.getVaultItemStateFlow(itemIdString).test { vaultRepository.getVaultItemStateFlow(itemIdString).test {
@ -1976,7 +1829,7 @@ class VaultRepositoryTest {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
syncService.sync() vaultSyncManager.sync(any(), any())
} }
} }
@ -2093,7 +1946,9 @@ class VaultRepositoryTest {
val folderId = 1234 val folderId = 1234
val folderIdString = "mockId-$folderId" val folderIdString = "mockId-$folderId"
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns UnknownHostException().asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(UnknownHostException())
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository.getVaultFolderStateFlow(folderIdString).test { vaultRepository.getVaultFolderStateFlow(folderIdString).test {
@ -2103,7 +1958,7 @@ class VaultRepositoryTest {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
syncService.sync() vaultSyncManager.sync(any(), any())
} }
} }
@ -2114,7 +1969,9 @@ class VaultRepositoryTest {
val folderIdString = "mockId-$folderId" val folderIdString = "mockId-$folderId"
val throwable = Throwable("Fail") val throwable = Throwable("Fail")
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns throwable.asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(throwable)
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository.getVaultFolderStateFlow(folderIdString).test { vaultRepository.getVaultFolderStateFlow(folderIdString).test {
@ -2124,7 +1981,7 @@ class VaultRepositoryTest {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
syncService.sync() vaultSyncManager.sync(any(), any())
} }
} }
@ -2169,7 +2026,9 @@ class VaultRepositoryTest {
runTest { runTest {
val sendId = 1234 val sendId = 1234
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns UnknownHostException().asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(UnknownHostException())
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository.getSendStateFlow("mockId-$sendId").test { vaultRepository.getSendStateFlow("mockId-$sendId").test {
@ -2179,7 +2038,7 @@ class VaultRepositoryTest {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
syncService.sync() vaultSyncManager.sync(any(), any())
} }
} }
@ -2189,7 +2048,9 @@ class VaultRepositoryTest {
val sendId = 1234 val sendId = 1234
val throwable = Throwable("Fail") val throwable = Throwable("Fail")
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { syncService.sync() } returns throwable.asFailure() coEvery {
vaultSyncManager.sync(any(), any())
} returns SyncVaultDataResult.Error(throwable)
setupVaultDiskSourceFlows() setupVaultDiskSourceFlows()
vaultRepository.getSendStateFlow("mockId-$sendId").test { vaultRepository.getSendStateFlow("mockId-$sendId").test {
@ -2199,7 +2060,7 @@ class VaultRepositoryTest {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
syncService.sync() vaultSyncManager.sync(any(), any())
} }
} }
@ -3175,29 +3036,9 @@ class VaultRepositoryTest {
val userId = "mockId-1" val userId = "mockId-1"
setVaultToUnlocked(userId = userId) setVaultToUnlocked(userId = userId)
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery { coEvery {
vaultSdkSource.initializeOrganizationCrypto( vaultSyncManager.sync(userId = userId, forced = false)
userId = userId, } returns SyncVaultDataResult.Success(itemsAvailable = true)
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
val stateFlow = MutableStateFlow<DataState<VerificationCodeItem?>>( val stateFlow = MutableStateFlow<DataState<VerificationCodeItem?>>(
DataState.Loading, DataState.Loading,
@ -3243,28 +3084,9 @@ class VaultRepositoryTest {
val userId = "mockId-1" val userId = "mockId-1"
setVaultToUnlocked(userId = userId) setVaultToUnlocked(userId = userId)
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery { coEvery {
vaultSdkSource.initializeOrganizationCrypto( vaultSyncManager.sync(any(), any())
userId = userId, } returns SyncVaultDataResult.Success(itemsAvailable = true)
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
val stateFlow = MutableStateFlow<DataState<List<VerificationCodeItem>>>( val stateFlow = MutableStateFlow<DataState<List<VerificationCodeItem>>>(
DataState.Loading, DataState.Loading,
@ -3308,11 +3130,11 @@ class VaultRepositoryTest {
every { every {
settingsDiskSource.getLastSyncTime(userId = userId) settingsDiskSource.getLastSyncTime(userId = userId)
} returns null } returns null
coEvery { syncService.sync() } just awaits coEvery { vaultSyncManager.sync(any(), any()) } just awaits
mutableFullSyncFlow.tryEmit(Unit) mutableFullSyncFlow.tryEmit(Unit)
coVerify { syncService.sync() } coVerify { vaultSyncManager.sync(any(), any()) }
} }
@Test @Test
@ -4393,31 +4215,12 @@ class VaultRepositoryTest {
runTest { runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1" val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { coEvery {
syncService.sync() vaultSyncManager.sync(
} returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId, userId = userId,
request = InitOrgCryptoRequest( forced = false,
organizationKeys = createMockOrganizationKeys(1),
),
) )
} returns InitializeCryptoResult.Success.asSuccess() } returns SyncVaultDataResult.Success(itemsAvailable = true)
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
every {
settingsDiskSource.storeLastSyncTime(
MOCK_USER_STATE.activeUserId,
clock.instant(),
)
} just runs
val syncResult = vaultRepository.syncForResult() val syncResult = vaultRepository.syncForResult()
assertEquals(SyncVaultDataResult.Success(itemsAvailable = true), syncResult) assertEquals(SyncVaultDataResult.Success(itemsAvailable = true), syncResult)
@ -4429,58 +4232,21 @@ class VaultRepositoryTest {
runTest { runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1" val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1).copy(ciphers = emptyList())
coEvery { coEvery {
syncService.sync() vaultSyncManager.sync(userId = userId, forced = false)
} returns mockSyncResponse.asSuccess() } returns SyncVaultDataResult.Success(itemsAvailable = false)
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
val syncResult = vaultRepository.syncForResult() val syncResult = vaultRepository.syncForResult()
assertEquals(SyncVaultDataResult.Success(itemsAvailable = false), syncResult) assertEquals(SyncVaultDataResult.Success(itemsAvailable = false), syncResult)
} }
@Test @Test
fun `syncForResult should return error when getAccountRevisionDateMillis fails`() = fun `syncForResult should return error when VaultSyncManager sync result is Error`() = runTest {
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 {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
val throwable = Throwable() val throwable = Throwable()
coEvery { coEvery {
syncService.sync() vaultSyncManager.sync(any(), any())
} returns throwable.asFailure() } returns SyncVaultDataResult.Error(throwable)
val syncResult = vaultRepository.syncForResult() val syncResult = vaultRepository.syncForResult()
assertEquals( assertEquals(
SyncVaultDataResult.Error(throwable = throwable), 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 //region Helper functions
/** /**
@ -4523,22 +4265,9 @@ class VaultRepositoryTest {
mockPin: String = "1234", mockPin: String = "1234",
) { ) {
val userId = "mockId-1" val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery { coEvery {
vaultSdkSource.initializeOrganizationCrypto( vaultSyncManager.sync(any(), any())
userId = userId, } returns SyncVaultDataResult.Success(itemsAvailable = true)
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = userId,
vault = mockSyncResponse,
)
} just runs
coEvery { coEvery {
vaultSdkSource.decryptSendList( vaultSdkSource.decryptSendList(
userId = userId, userId = userId,

View File

@ -9,8 +9,8 @@ import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository 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.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.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManagerImpl import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManagerImpl
import io.mockk.coEvery import io.mockk.coEvery