mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -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.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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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> =
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user