Refactor Folder logic into FolderManager (#5904)

This commit is contained in:
David Perez 2025-09-19 10:37:31 -05:00 committed by GitHub
parent 4f244c52fa
commit d53f3f313c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 837 additions and 763 deletions

View File

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
/**
* Manages the creating, updating, and deleting folders.
*/
interface FolderManager {
/**
* Attempt to create a folder.
*/
suspend fun createFolder(folderView: FolderView): CreateFolderResult
/**
* Attempt to delete a folder.
*/
suspend fun deleteFolder(folderId: String): DeleteFolderResult
/**
* Attempt to update a folder.
*/
suspend fun updateFolder(folderId: String, folderView: FolderView): UpdateFolderResult
}

View File

@ -0,0 +1,170 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.model.UpdateFolderResponseJson
import com.bitwarden.network.service.FolderService
import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* The default implementation of the [FolderManager].
*/
class FolderManagerImpl(
private val authDiskSource: AuthDiskSource,
private val folderService: FolderService,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
dispatcherManager: DispatcherManager,
pushManager: PushManager,
) : FolderManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val ioScope = CoroutineScope(dispatcherManager.io)
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
init {
pushManager
.syncFolderDeleteFlow
.onEach(::deleteFolder)
.launchIn(unconfinedScope)
pushManager
.syncFolderUpsertFlow
.onEach(::syncFolderIfNecessary)
.launchIn(ioScope)
}
override suspend fun createFolder(folderView: FolderView): CreateFolderResult {
val userId = activeUserId ?: return CreateFolderResult.Error(NoActiveUserException())
return vaultSdkSource
.encryptFolder(userId = userId, folder = folderView)
.flatMap { folderService.createFolder(body = it.toEncryptedNetworkFolder()) }
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
.flatMap {
vaultSdkSource.decryptFolder(userId = userId, folder = it.toEncryptedSdkFolder())
}
.fold(
onSuccess = { CreateFolderResult.Success(folderView = it) },
onFailure = { CreateFolderResult.Error(error = it) },
)
}
override suspend fun deleteFolder(folderId: String): DeleteFolderResult {
val userId = activeUserId ?: return DeleteFolderResult.Error(NoActiveUserException())
return folderService
.deleteFolder(folderId = folderId)
.onSuccess {
clearFolderIdFromCiphers(userId = userId, folderId = folderId)
vaultDiskSource.deleteFolder(userId = userId, folderId = folderId)
}
.fold(
onSuccess = { DeleteFolderResult.Success },
onFailure = { DeleteFolderResult.Error(error = it) },
)
}
override suspend fun updateFolder(
folderId: String,
folderView: FolderView,
): UpdateFolderResult {
val userId = activeUserId ?: return UpdateFolderResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return vaultSdkSource
.encryptFolder(userId = userId, folder = folderView)
.flatMap { folder ->
folderService.updateFolder(
folderId = folder.id.toString(),
body = folder.toEncryptedNetworkFolder(),
)
}
.fold(
onSuccess = { response ->
when (response) {
is UpdateFolderResponseJson.Success -> {
vaultDiskSource.saveFolder(userId = userId, folder = response.folder)
vaultSdkSource
.decryptFolder(
userId = userId,
folder = response.folder.toEncryptedSdkFolder(),
)
.fold(
onSuccess = { UpdateFolderResult.Success(it) },
onFailure = {
UpdateFolderResult.Error(errorMessage = null, error = it)
},
)
}
is UpdateFolderResponseJson.Invalid -> {
UpdateFolderResult.Error(errorMessage = response.message, error = null)
}
}
},
onFailure = { UpdateFolderResult.Error(it.message, error = it) },
)
}
private suspend fun clearFolderIdFromCiphers(userId: String, folderId: String) {
vaultDiskSource.getCiphers(userId = userId).forEach {
if (it.folderId == folderId) {
vaultDiskSource.saveCipher(userId = userId, cipher = it.copy(folderId = null))
}
}
}
/**
* Deletes the folder specified by [syncFolderDeleteData] from disk.
*/
private suspend fun deleteFolder(syncFolderDeleteData: SyncFolderDeleteData) {
clearFolderIdFromCiphers(
folderId = syncFolderDeleteData.folderId,
userId = syncFolderDeleteData.userId,
)
vaultDiskSource.deleteFolder(
folderId = syncFolderDeleteData.folderId,
userId = syncFolderDeleteData.userId,
)
}
/**
* Syncs an individual folder contained in [syncFolderUpsertData] to disk if certain criteria
* are met.
*/
private suspend fun syncFolderIfNecessary(syncFolderUpsertData: SyncFolderUpsertData) {
val userId = activeUserId ?: return
val folderId = syncFolderUpsertData.folderId
val isUpdate = syncFolderUpsertData.isUpdate
val revisionDate = syncFolderUpsertData.revisionDate
val localFolder = vaultDiskSource
.getFolders(userId = userId)
.first()
.find { it.id == folderId }
val isValidCreate = !isUpdate && localFolder == null
val isValidUpdate = isUpdate &&
localFolder != null &&
localFolder.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
if (!isValidCreate && !isValidUpdate) return
folderService
.getFolder(folderId = folderId)
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
}
}

View File

@ -5,8 +5,9 @@ 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.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.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
@ -24,6 +25,8 @@ import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManagerImpl import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManagerImpl
import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.FolderManagerImpl
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
@ -71,6 +74,24 @@ object VaultManagerModule {
pushManager = pushManager, pushManager = pushManager,
) )
@Provides
@Singleton
fun provideFolderManager(
folderService: FolderService,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
dispatcherManager: DispatcherManager,
pushManager: PushManager,
): FolderManager = FolderManagerImpl(
authDiskSource = authDiskSource,
folderService = folderService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
dispatcherManager = dispatcherManager,
pushManager = pushManager,
)
@Provides @Provides
@Singleton @Singleton
fun provideSendManager( fun provideSendManager(

View File

@ -13,19 +13,17 @@ 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.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
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.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.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
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.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.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@ -37,7 +35,7 @@ import javax.crypto.Cipher
* Responsible for managing vault data inside the network layer. * Responsible for managing vault data inside the network layer.
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface VaultRepository : CipherManager, SendManager, VaultLockManager { interface VaultRepository : CipherManager, FolderManager, SendManager, VaultLockManager {
/** /**
* The [VaultFilterType] for the current user. * The [VaultFilterType] for the current user.
@ -204,21 +202,6 @@ interface VaultRepository : CipherManager, SendManager, VaultLockManager {
*/ */
suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult
/**
* Attempt to create a folder.
*/
suspend fun createFolder(folderView: FolderView): CreateFolderResult
/**
* Attempt to delete a folder.
*/
suspend fun deleteFolder(folderId: String): DeleteFolderResult
/**
* Attempt to update a folder.
*/
suspend fun updateFolder(folderId: String, folderView: FolderView): UpdateFolderResult
/** /**
* Attempt to get the user's vault data for export. * Attempt to get the user's vault data for export.
* *

View File

@ -10,12 +10,9 @@ import com.bitwarden.core.data.repository.util.map
import com.bitwarden.core.data.repository.util.mapNullable import com.bitwarden.core.data.repository.util.mapNullable
import com.bitwarden.core.data.repository.util.updateToPendingOrLoading import com.bitwarden.core.data.repository.util.updateToPendingOrLoading
import com.bitwarden.core.data.util.asFailure import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.data.manager.DispatcherManager import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.exporters.ExportFormat import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.network.model.UpdateFolderResponseJson
import com.bitwarden.network.service.FolderService
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.SendView import com.bitwarden.send.SendView
@ -34,14 +31,13 @@ import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.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
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked
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.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
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
@ -50,21 +46,17 @@ import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult 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.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
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.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.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByTypeAndOrganization import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByTypeAndOrganization
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
@ -87,7 +79,6 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ -116,12 +107,12 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
*/ */
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass") @Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
class VaultRepositoryImpl( class VaultRepositoryImpl(
private val folderService: FolderService,
private val vaultDiskSource: VaultDiskSource, private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource, private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource, private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource, private val settingsDiskSource: SettingsDiskSource,
private val cipherManager: CipherManager, private val cipherManager: CipherManager,
private val folderManager: FolderManager,
private val sendManager: SendManager, private val sendManager: SendManager,
private val vaultLockManager: VaultLockManager, private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager, private val totpCodeManager: TotpCodeManager,
@ -133,6 +124,7 @@ class VaultRepositoryImpl(
private val credentialExchangeImportManager: CredentialExchangeImportManager, private val credentialExchangeImportManager: CredentialExchangeImportManager,
) : VaultRepository, ) : VaultRepository,
CipherManager by cipherManager, CipherManager by cipherManager,
FolderManager by folderManager,
SendManager by sendManager, SendManager by sendManager,
VaultLockManager by vaultLockManager { VaultLockManager by vaultLockManager {
@ -281,16 +273,6 @@ class VaultRepositoryImpl(
.onEach { sync(forced = false) } .onEach { sync(forced = false) }
.launchIn(unconfinedScope) .launchIn(unconfinedScope)
pushManager
.syncFolderDeleteFlow
.onEach(::deleteFolder)
.launchIn(unconfinedScope)
pushManager
.syncFolderUpsertFlow
.onEach(::syncFolderIfNecessary)
.launchIn(ioScope)
databaseSchemeManager databaseSchemeManager
.databaseSchemeChangeFlow .databaseSchemeChangeFlow
.onEach { sync(forced = true) } .onEach { sync(forced = true) }
@ -645,106 +627,6 @@ class VaultRepositoryImpl(
) )
} }
override suspend fun createFolder(folderView: FolderView): CreateFolderResult {
val userId = activeUserId
?: return CreateFolderResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptFolder(
userId = userId,
folder = folderView,
)
.flatMap { folder ->
folderService
.createFolder(
body = folder.toEncryptedNetworkFolder(),
)
}
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
.flatMap { vaultSdkSource.decryptFolder(userId, it.toEncryptedSdkFolder()) }
.fold(
onSuccess = { CreateFolderResult.Success(folderView = it) },
onFailure = { CreateFolderResult.Error(error = it) },
)
}
override suspend fun updateFolder(
folderId: String,
folderView: FolderView,
): UpdateFolderResult {
val userId = activeUserId ?: return UpdateFolderResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return vaultSdkSource
.encryptFolder(
userId = userId,
folder = folderView,
)
.flatMap { folder ->
folderService
.updateFolder(
folderId = folder.id.toString(),
body = folder.toEncryptedNetworkFolder(),
)
}
.fold(
onSuccess = { response ->
when (response) {
is UpdateFolderResponseJson.Success -> {
vaultDiskSource.saveFolder(userId, response.folder)
vaultSdkSource
.decryptFolder(
userId,
response.folder.toEncryptedSdkFolder(),
)
.fold(
onSuccess = { UpdateFolderResult.Success(it) },
onFailure = {
UpdateFolderResult.Error(
errorMessage = null,
error = it,
)
},
)
}
is UpdateFolderResponseJson.Invalid -> {
UpdateFolderResult.Error(
errorMessage = response.message,
error = null,
)
}
}
},
onFailure = { UpdateFolderResult.Error(it.message, error = it) },
)
}
override suspend fun deleteFolder(folderId: String): DeleteFolderResult {
val userId = activeUserId
?: return DeleteFolderResult.Error(error = NoActiveUserException())
return folderService
.deleteFolder(
folderId = folderId,
)
.onSuccess {
clearFolderIdFromCiphers(folderId, userId)
vaultDiskSource.deleteFolder(userId, folderId)
}
.fold(
onSuccess = { DeleteFolderResult.Success },
onFailure = { DeleteFolderResult.Error(error = it) },
)
}
private suspend fun clearFolderIdFromCiphers(folderId: String, userId: String) {
vaultDiskSource.getCiphers(userId = userId).forEach {
if (it.folderId == folderId) {
vaultDiskSource.saveCipher(userId = userId, cipher = it.copy(folderId = null))
}
}
}
override suspend fun exportVaultDataToString( override suspend fun exportVaultDataToString(
format: ExportFormat, format: ExportFormat,
restrictedTypes: List<CipherType>, restrictedTypes: List<CipherType>,
@ -1049,48 +931,6 @@ class VaultRepositoryImpl(
} }
?: DataState.Loading ?: DataState.Loading
//region Push notification helpers
/**
* Deletes the folder specified by [syncFolderDeleteData] from disk.
*/
private suspend fun deleteFolder(syncFolderDeleteData: SyncFolderDeleteData) {
clearFolderIdFromCiphers(
folderId = syncFolderDeleteData.folderId,
userId = syncFolderDeleteData.userId,
)
vaultDiskSource.deleteFolder(
folderId = syncFolderDeleteData.folderId,
userId = syncFolderDeleteData.userId,
)
}
/**
* Syncs an individual folder contained in [syncFolderUpsertData] to disk if certain criteria
* are met.
*/
private suspend fun syncFolderIfNecessary(syncFolderUpsertData: SyncFolderUpsertData) {
val userId = activeUserId ?: return
val folderId = syncFolderUpsertData.folderId
val isUpdate = syncFolderUpsertData.isUpdate
val revisionDate = syncFolderUpsertData.revisionDate
val localFolder = vaultDiskSource
.getFolders(userId = userId)
.first()
.find { it.id == folderId }
val isValidCreate = !isUpdate && localFolder == null
val isValidUpdate = isUpdate &&
localFolder != null &&
localFolder.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
if (!isValidCreate && !isValidUpdate) return
folderService
.getFolder(folderId = folderId)
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
}
//endregion Push Notification helpers
private suspend fun syncInternal( private suspend fun syncInternal(
userId: String, userId: String,
forced: Boolean, forced: Boolean,

View File

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.vault.repository.di package com.x8bit.bitwarden.data.vault.repository.di
import com.bitwarden.data.manager.DispatcherManager import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.FolderService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
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
@ -10,6 +9,7 @@ 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.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
import com.x8bit.bitwarden.data.vault.manager.FolderManager
import com.x8bit.bitwarden.data.vault.manager.SendManager import com.x8bit.bitwarden.data.vault.manager.SendManager
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
@ -33,12 +33,12 @@ object VaultRepositoryModule {
@Provides @Provides
@Singleton @Singleton
fun providesVaultRepository( fun providesVaultRepository(
folderService: FolderService,
vaultDiskSource: VaultDiskSource, vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource, vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource, authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource, settingsDiskSource: SettingsDiskSource,
cipherManager: CipherManager, cipherManager: CipherManager,
folderManager: FolderManager,
sendManager: SendManager, sendManager: SendManager,
vaultLockManager: VaultLockManager, vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
@ -49,12 +49,12 @@ object VaultRepositoryModule {
vaultSyncManager: VaultSyncManager, vaultSyncManager: VaultSyncManager,
credentialExchangeImportManager: CredentialExchangeImportManager, credentialExchangeImportManager: CredentialExchangeImportManager,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
folderService = folderService,
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource, settingsDiskSource = settingsDiskSource,
cipherManager = cipherManager, cipherManager = cipherManager,
folderManager = folderManager,
sendManager = sendManager, sendManager = sendManager,
vaultLockManager = vaultLockManager, vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,

View File

@ -0,0 +1,610 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.network.model.FolderJsonRequest
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateFolderResponseJson
import com.bitwarden.network.model.createMockCipher
import com.bitwarden.network.model.createMockFolder
import com.bitwarden.network.service.FolderService
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView
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.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.runs
import io.mockk.unmockkConstructor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
class FolderManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val folderService = mockk<FolderService>()
private val vaultDiskSource = mockk<VaultDiskSource>()
private val vaultSdkSource = mockk<VaultSdkSource>()
private val mutableSyncFolderDeleteFlow = bufferedMutableSharedFlow<SyncFolderDeleteData>()
private val mutableSyncFolderUpsertFlow = bufferedMutableSharedFlow<SyncFolderUpsertData>()
private val pushManager: PushManager = mockk {
every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
}
private val folderManager: FolderManager = FolderManagerImpl(
authDiskSource = fakeAuthDiskSource,
folderService = folderService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
pushManager = pushManager,
dispatcherManager = FakeDispatcherManager(),
)
@BeforeEach
fun setup() {
mockkConstructor(NoActiveUserException::class)
every {
anyConstructed<NoActiveUserException>() == any<NoActiveUserException>()
} returns true
}
@AfterEach
fun tearDown() {
unmockkConstructor(NoActiveUserException::class)
}
@Test
fun `createFolder with no active user should return CreateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = null
val result = folderManager.createFolder(folderView = mockk())
assertEquals(CreateFolderResult.Error(error = NoActiveUserException()), result)
}
@Suppress("MaxLineLength")
@Test
fun `createFolder with folderService Delete failure should return DeleteFolderResult Failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderId = "mockId-1"
val error = Throwable("fail")
coEvery { folderService.deleteFolder(folderId = folderId) } returns error.asFailure()
val result = folderManager.deleteFolder(folderId = folderId)
assertEquals(DeleteFolderResult.Error(error = error), result)
}
@Test
fun `createFolder with encryptFolder failure should return CreateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = null,
name = "TestName",
revisionDate = Instant.now(FIXED_CLOCK),
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns error.asFailure()
val result = folderManager.createFolder(folderView = folderView)
assertEquals(CreateFolderResult.Error(error = error), result)
}
@Test
fun `createFolder with folderService failure should return CreateFolderResult failure`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val testFolderName = "TestName"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = null,
name = testFolderName,
revisionDate = date,
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns Folder(id = null, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.createFolder(body = FolderJsonRequest(name = testFolderName))
} returns error.asFailure()
val result = folderManager.createFolder(folderView = folderView)
assertEquals(CreateFolderResult.Error(error = error), result)
}
@Test
fun `createFolder with folderService createFolder should return CreateFolderResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val date = Instant.now(FIXED_CLOCK)
val testFolderName = "TestName"
val folderView = FolderView(
id = null,
name = testFolderName,
revisionDate = date,
)
val networkFolder = SyncResponseJson.Folder(
id = "1",
name = testFolderName,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
)
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns Folder(id = null, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.createFolder(body = FolderJsonRequest(name = testFolderName))
} returns networkFolder.asSuccess()
coEvery {
vaultDiskSource.saveFolder(userId = ACTIVE_USER_ID, folder = networkFolder)
} just runs
coEvery {
vaultSdkSource.decryptFolder(
userId = ACTIVE_USER_ID,
folder = networkFolder.toEncryptedSdkFolder(),
)
} returns folderView.asSuccess()
val result = folderManager.createFolder(folderView = folderView)
assertEquals(CreateFolderResult.Success(folderView = folderView), result)
}
@Test
fun `deleteFolder with no active user should return DeleteFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = null
val result = folderManager.deleteFolder(folderId = "Test")
assertEquals(DeleteFolderResult.Error(error = NoActiveUserException()), result)
}
@Suppress("MaxLineLength")
@Test
fun `DeleteFolder with folderService Delete failure should return DeleteFolderResult Failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val error = Throwable("fail")
val folderId = "mockId-1"
coEvery { folderService.deleteFolder(folderId = folderId) } returns error.asFailure()
val result = folderManager.deleteFolder(folderId = folderId)
assertEquals(DeleteFolderResult.Error(error = error), result)
}
@Suppress("MaxLineLength")
@Test
fun `DeleteFolder with folderService Delete success should return DeleteFolderResult Success and update ciphers`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = ACTIVE_USER_ID
val folderId = "mockFolderId-1"
val mockCipher = createMockCipher(number = 1)
val ciphers = listOf(mockCipher, createMockCipher(number = 2))
coEvery { folderService.deleteFolder(folderId = folderId) } returns Unit.asSuccess()
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns ciphers
coEvery {
vaultDiskSource.saveCipher(
userId = userId,
cipher = mockCipher.copy(folderId = null),
)
} just runs
val result = folderManager.deleteFolder(folderId = folderId)
coVerify(exactly = 1) {
vaultDiskSource.saveCipher(
userId = userId,
cipher = mockCipher.copy(folderId = null),
)
}
assertEquals(DeleteFolderResult.Success, result)
}
@Test
fun `updateFolder with no active user should return UpdateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = null
val result = folderManager.updateFolder(folderId = "Test", folderView = mockk())
assertEquals(
UpdateFolderResult.Error(errorMessage = null, error = NoActiveUserException()),
result,
)
}
@Test
fun `updateFolder with encryptFolder failure should return UpdateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderId = "testId"
val folderView = FolderView(
id = folderId,
name = "TestName",
revisionDate = Instant.now(FIXED_CLOCK),
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns error.asFailure()
val result = folderManager.updateFolder(folderId = folderId, folderView = folderView)
assertEquals(UpdateFolderResult.Error(errorMessage = null, error = error), result)
}
@Test
fun `updateFolder with folderService failure should return UpdateFolderResult failure`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val testFolderName = "TestName"
val folderId = "testId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = folderId,
name = testFolderName,
revisionDate = date,
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.updateFolder(
folderId = folderId,
body = FolderJsonRequest(name = testFolderName),
)
} returns error.asFailure()
val result = folderManager.updateFolder(folderId = folderId, folderView = folderView)
assertEquals(UpdateFolderResult.Error(errorMessage = null, error = error), result)
}
@Suppress("MaxLineLength")
@Test
fun `updateFolder with folderService updateFolder Invalid response should return UpdateFolderResult Error with a non-null message`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val testFolderName = "TestName"
val folderId = "testId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = folderId,
name = testFolderName,
revisionDate = date,
)
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.updateFolder(
folderId = folderId,
body = FolderJsonRequest(name = testFolderName),
)
} returns UpdateFolderResponseJson
.Invalid(
message = "You do not have permission to edit this.",
validationErrors = null,
)
.asSuccess()
val result = folderManager.updateFolder(folderId = folderId, folderView = folderView)
assertEquals(
UpdateFolderResult.Error(
errorMessage = "You do not have permission to edit this.",
error = null,
),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `updateFolder with folderService updateFolder success should return UpdateFolderResult success`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val testFolderName = "TestName"
val folderId = "testId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = folderId,
name = testFolderName,
revisionDate = date,
)
val networkFolder = SyncResponseJson.Folder(
id = "1",
name = testFolderName,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
)
coEvery {
vaultSdkSource.encryptFolder(userId = ACTIVE_USER_ID, folder = folderView)
} returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.updateFolder(
folderId = folderId,
body = FolderJsonRequest(name = testFolderName),
)
} returns UpdateFolderResponseJson
.Success(folder = networkFolder)
.asSuccess()
coEvery {
vaultDiskSource.saveFolder(userId = ACTIVE_USER_ID, folder = networkFolder)
} just runs
coEvery {
vaultSdkSource.decryptFolder(
userId = ACTIVE_USER_ID,
folder = networkFolder.toEncryptedSdkFolder(),
)
} returns folderView.asSuccess()
val result = folderManager.updateFolder(folderId = folderId, folderView = folderView)
assertEquals(UpdateFolderResult.Success(folderView = folderView), result)
}
@Test
fun `syncFolderDeleteFlow should delete folder from disk and update ciphers`() {
val userId = "mockId-1"
val folderId = "mockId-1"
val cipher = createMockCipher(number = 1, folderId = folderId)
val updatedCipher = createMockCipher(number = 1, folderId = null)
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns listOf(cipher)
coEvery { vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher) } just runs
mutableSyncFolderDeleteFlow.tryEmit(
SyncFolderDeleteData(userId = userId, folderId = folderId),
)
coVerify(exactly = 1) {
vaultDiskSource.deleteFolder(userId = userId, folderId = folderId)
vaultDiskSource.getCiphers(userId = userId)
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
}
}
@Test
fun `syncFolderUpsertFlow create with local folder should do nothing`() = runTest {
val number = 1
val userId = ACTIVE_USER_ID
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folder = createMockFolder(number = number)
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(listOf(folder))
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
),
)
coVerify(exactly = 0) {
folderService.getFolder(folderId = any())
vaultDiskSource.saveFolder(userId = any(), folder = any())
}
}
@Test
fun `syncFolderUpsertFlow update with no local folder should do nothing`() = runTest {
val number = 1
val userId = ACTIVE_USER_ID
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(emptyList())
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
),
)
coVerify(exactly = 0) {
folderService.getFolder(folderId = any())
vaultDiskSource.saveFolder(userId = any(), folder = any())
}
}
@Test
fun `syncFolderUpsertFlow update with more recent local folder should do nothing`() = runTest {
val number = 1
val userId = ACTIVE_USER_ID
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folder = createMockFolder(number = number)
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(listOf(folder))
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.ofInstant(
Instant.ofEpochSecond(0), ZoneId.of("UTC"),
),
isUpdate = true,
),
)
coVerify(exactly = 0) {
folderService.getFolder(folderId = any())
vaultDiskSource.saveFolder(userId = any(), folder = any())
}
}
@Suppress("MaxLineLength")
@Test
fun `syncFolderUpsertFlow valid create success should make a request for a folder and then store it`() =
runTest {
val number = 1
val userId = ACTIVE_USER_ID
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(emptyList())
val folder = mockk<SyncResponseJson.Folder>()
coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess()
coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
),
)
coVerify(exactly = 1) {
folderService.getFolder(folderId = folderId)
vaultDiskSource.saveFolder(userId = userId, folder = folder)
}
}
@Suppress("MaxLineLength")
@Test
fun `syncFolderUpsertFlow valid update success should make a request for a folder and then store it`() =
runTest {
val number = 1
val userId = ACTIVE_USER_ID
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = createMockFolder(
number = number,
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
)
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(listOf(folderView))
val folder = mockk<SyncResponseJson.Folder>()
coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess()
coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
),
)
coVerify(exactly = 1) {
folderService.getFolder(folderId = folderId)
vaultDiskSource.saveFolder(userId = userId, folder = folder)
}
}
}
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private const val ACTIVE_USER_ID: String = "mockId-1"
private val MOCK_PROFILE = AccountJson.Profile(
userId = ACTIVE_USER_ID,
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 = ACTIVE_USER_ID,
accounts = mapOf(
ACTIVE_USER_ID to MOCK_ACCOUNT,
),
)

View File

@ -15,22 +15,18 @@ import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.exporters.ExportFormat import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.network.model.CipherTypeJson import com.bitwarden.network.model.CipherTypeJson
import com.bitwarden.network.model.FolderJsonRequest
import com.bitwarden.network.model.SyncResponseJson import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateFolderResponseJson
import com.bitwarden.network.model.createMockCipher import com.bitwarden.network.model.createMockCipher
import com.bitwarden.network.model.createMockCollection import com.bitwarden.network.model.createMockCollection
import com.bitwarden.network.model.createMockDomains import com.bitwarden.network.model.createMockDomains
import com.bitwarden.network.model.createMockFolder import com.bitwarden.network.model.createMockFolder
import com.bitwarden.network.model.createMockOrganizationKeys import com.bitwarden.network.model.createMockOrganizationKeys
import com.bitwarden.network.model.createMockSend import com.bitwarden.network.model.createMockSend
import com.bitwarden.network.service.FolderService
import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherType import com.bitwarden.vault.CipherType
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.bitwarden.vault.DecryptCipherListResult import com.bitwarden.vault.DecryptCipherListResult
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView import com.bitwarden.vault.FolderView
import com.bitwarden.vault.TotpResponse import com.bitwarden.vault.TotpResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
@ -43,8 +39,6 @@ import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.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
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
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.createMockAccount import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAccount
@ -65,14 +59,11 @@ import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult 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.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
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.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
@ -81,7 +72,6 @@ import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
@ -113,7 +103,6 @@ import java.security.GeneralSecurityException
import java.security.MessageDigest import java.security.MessageDigest
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@ -131,7 +120,6 @@ class VaultRepositoryTest {
private val settingsDiskSource = mockk<SettingsDiskSource> { private val settingsDiskSource = mockk<SettingsDiskSource> {
every { getLastSyncTime(userId = any()) } returns clock.instant() every { getLastSyncTime(userId = any()) } returns clock.instant()
} }
private val folderService: FolderService = mockk()
private val mutableGetCiphersFlow: MutableStateFlow<List<SyncResponseJson.Cipher>> = private val mutableGetCiphersFlow: MutableStateFlow<List<SyncResponseJson.Cipher>> =
MutableStateFlow(listOf(createMockCipher(1))) MutableStateFlow(listOf(createMockCipher(1)))
private val vaultDiskSource: VaultDiskSource = mockk { private val vaultDiskSource: VaultDiskSource = mockk {
@ -169,18 +157,13 @@ class VaultRepositoryTest {
every { databaseSchemeChangeFlow } returns mutableDatabaseSchemeChangeFlow every { databaseSchemeChangeFlow } returns mutableDatabaseSchemeChangeFlow
} }
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>() private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
private val mutableSyncFolderDeleteFlow = bufferedMutableSharedFlow<SyncFolderDeleteData>()
private val mutableSyncFolderUpsertFlow = bufferedMutableSharedFlow<SyncFolderUpsertData>()
private val pushManager: PushManager = mockk { private val pushManager: PushManager = mockk {
every { fullSyncFlow } returns mutableFullSyncFlow every { fullSyncFlow } returns mutableFullSyncFlow
every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
} }
private val vaultSyncManager: VaultSyncManager = mockk() private val vaultSyncManager: VaultSyncManager = mockk()
private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk() private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk()
private val vaultRepository = VaultRepositoryImpl( private val vaultRepository = VaultRepositoryImpl(
folderService = folderService,
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
authDiskSource = fakeAuthDiskSource, authDiskSource = fakeAuthDiskSource,
@ -190,6 +173,7 @@ class VaultRepositoryTest {
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
pushManager = pushManager, pushManager = pushManager,
cipherManager = mockk(), cipherManager = mockk(),
folderManager = mockk(),
sendManager = mockk(), sendManager = mockk(),
clock = clock, clock = clock,
databaseSchemeManager = databaseSchemeManager, databaseSchemeManager = databaseSchemeManager,
@ -2063,362 +2047,6 @@ class VaultRepositoryTest {
) )
} }
@Test
fun `deleteFolder with no active user should return DeleteFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = null
val result = vaultRepository.deleteFolder("Test")
assertEquals(
DeleteFolderResult.Error(error = NoActiveUserException()),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `DeleteFolder with folderService Delete failure should return DeleteFolderResult Failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val error = Throwable("fail")
val folderId = "mockId-1"
coEvery { folderService.deleteFolder(folderId) } returns error.asFailure()
val result = vaultRepository.deleteFolder(folderId)
assertEquals(DeleteFolderResult.Error(error = error), result)
}
@Suppress("MaxLineLength")
@Test
fun `DeleteFolder with folderService Delete success should return DeleteFolderResult Success and update ciphers`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = MOCK_USER_STATE.activeUserId
val folderId = "mockFolderId-1"
val mockCipher = createMockCipher(number = 1)
val ciphers = listOf(mockCipher, createMockCipher(number = 2))
coEvery { folderService.deleteFolder(folderId) } returns Unit.asSuccess()
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns ciphers
coEvery {
vaultDiskSource.saveCipher(
userId = userId,
cipher = mockCipher.copy(folderId = null),
)
} just runs
val result = vaultRepository.deleteFolder(folderId = folderId)
coVerify(exactly = 1) {
vaultDiskSource.saveCipher(
userId = userId,
cipher = mockCipher.copy(folderId = null),
)
}
assertEquals(DeleteFolderResult.Success, result)
}
@Test
fun `createFolder with no active user should return CreateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = null
val result = vaultRepository.createFolder(mockk())
assertEquals(
CreateFolderResult.Error(NoActiveUserException()),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `createFolder with folderService Delete failure should return DeleteFolderResult Failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderId = "mockId-1"
val error = Throwable("fail")
coEvery { folderService.deleteFolder(folderId) } returns error.asFailure()
val result = vaultRepository.deleteFolder(folderId)
assertEquals(DeleteFolderResult.Error(error = error), result)
}
@Test
fun `createFolder with encryptFolder failure should return CreateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = null,
name = "TestName",
revisionDate = DateTime.now(),
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns error.asFailure()
val result = vaultRepository.createFolder(folderView)
assertEquals(CreateFolderResult.Error(error = error), result)
}
@Test
fun `createFolder with folderService failure should return CreateFolderResult failure`() =
runTest {
val date = DateTime.now()
val testFolderName = "TestName"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = null,
name = testFolderName,
revisionDate = date,
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns Folder(id = null, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.createFolder(
body = FolderJsonRequest(testFolderName),
)
} returns error.asFailure()
val result = vaultRepository.createFolder(folderView)
assertEquals(CreateFolderResult.Error(error = error), result)
}
@Test
fun `createFolder with folderService createFolder should return CreateFolderResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val date = DateTime.now()
val testFolderName = "TestName"
val folderView = FolderView(
id = null,
name = testFolderName,
revisionDate = date,
)
val networkFolder = SyncResponseJson.Folder(
id = "1",
name = testFolderName,
revisionDate = ZonedDateTime.now(),
)
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns Folder(id = null, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.createFolder(
body = FolderJsonRequest(testFolderName),
)
} returns networkFolder.asSuccess()
coEvery {
vaultDiskSource.saveFolder(
MOCK_USER_STATE.activeUserId,
networkFolder,
)
} just runs
coEvery {
vaultSdkSource.decryptFolder(
MOCK_USER_STATE.activeUserId,
networkFolder.toEncryptedSdkFolder(),
)
} returns folderView.asSuccess()
val result = vaultRepository.createFolder(folderView)
assertEquals(CreateFolderResult.Success(folderView), result)
}
@Test
fun `updateFolder with no active user should return UpdateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = null
val result = vaultRepository.updateFolder("Test", mockk())
assertEquals(
UpdateFolderResult.Error(errorMessage = null, error = NoActiveUserException()),
result,
)
}
@Test
fun `updateFolder with encryptFolder failure should return UpdateFolderResult failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderId = "testId"
val folderView = FolderView(
id = folderId,
name = "TestName",
revisionDate = DateTime.now(),
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns error.asFailure()
val result = vaultRepository.updateFolder(folderId, folderView)
assertEquals(UpdateFolderResult.Error(errorMessage = null, error = error), result)
}
@Test
fun `updateFolder with folderService failure should return UpdateFolderResult failure`() =
runTest {
val date = DateTime.now()
val testFolderName = "TestName"
val folderId = "testId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = folderId,
name = testFolderName,
revisionDate = date,
)
val error = IllegalStateException()
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.updateFolder(
folderId = folderId,
body = FolderJsonRequest(testFolderName),
)
} returns error.asFailure()
val result = vaultRepository.updateFolder(folderId, folderView)
assertEquals(UpdateFolderResult.Error(errorMessage = null, error = error), result)
}
@Suppress("MaxLineLength")
@Test
fun `updateFolder with folderService updateFolder Invalid response should return UpdateFolderResult Error with a non-null message`() =
runTest {
val date = DateTime.now()
val testFolderName = "TestName"
val folderId = "testId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = folderId,
name = testFolderName,
revisionDate = date,
)
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.updateFolder(
folderId = folderId,
body = FolderJsonRequest(testFolderName),
)
} returns UpdateFolderResponseJson
.Invalid(
message = "You do not have permission to edit this.",
validationErrors = null,
)
.asSuccess()
val result = vaultRepository.updateFolder(folderId, folderView)
assertEquals(
UpdateFolderResult.Error(
errorMessage = "You do not have permission to edit this.",
error = null,
),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `updateFolder with folderService updateFolder success should return UpdateFolderResult success`() =
runTest {
val date = DateTime.now()
val testFolderName = "TestName"
val folderId = "testId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = FolderView(
id = folderId,
name = testFolderName,
revisionDate = date,
)
val networkFolder = SyncResponseJson.Folder(
id = "1",
name = testFolderName,
revisionDate = ZonedDateTime.now(),
)
coEvery {
vaultSdkSource.encryptFolder(
userId = MOCK_USER_STATE.activeUserId,
folder = folderView,
)
} returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess()
coEvery {
folderService.updateFolder(
folderId = folderId,
body = FolderJsonRequest(testFolderName),
)
} returns UpdateFolderResponseJson
.Success(folder = networkFolder)
.asSuccess()
coEvery {
vaultDiskSource.saveFolder(
MOCK_USER_STATE.activeUserId,
networkFolder,
)
} just runs
coEvery {
vaultSdkSource.decryptFolder(
MOCK_USER_STATE.activeUserId,
networkFolder.toEncryptedSdkFolder(),
)
} returns folderView.asSuccess()
val result = vaultRepository.updateFolder(folderId, folderView)
assertEquals(UpdateFolderResult.Success(folderView), result)
}
@Test @Test
fun `getAuthCodeFlow with no active user should emit an error`() = runTest { fun `getAuthCodeFlow with no active user should emit an error`() = runTest {
fakeAuthDiskSource.userState = null fakeAuthDiskSource.userState = null
@ -2533,210 +2161,6 @@ class VaultRepositoryTest {
coVerify { vaultSyncManager.sync(any(), any()) } coVerify { vaultSyncManager.sync(any(), any()) }
} }
@Test
fun `syncFolderDeleteFlow should delete folder from disk and update ciphers`() {
val userId = "mockId-1"
val folderId = "mockId-1"
val cipher = createMockCipher(number = 1, folderId = folderId)
val updatedCipher = createMockCipher(number = 1, folderId = null)
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns listOf(cipher)
coEvery { vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher) } just runs
mutableSyncFolderDeleteFlow.tryEmit(
SyncFolderDeleteData(userId = userId, folderId = folderId),
)
coVerify(exactly = 1) {
vaultDiskSource.deleteFolder(userId = userId, folderId = folderId)
vaultDiskSource.getCiphers(userId = userId)
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
}
}
@Test
fun `syncFolderUpsertFlow create with local folder should do nothing`() = runTest {
val number = 1
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
val folderView = createMockFolderView(number = number)
coEvery {
vaultSdkSource.decryptFolderList(
userId = MOCK_USER_STATE.activeUserId,
folderList = listOf(createMockSdkFolder(number = number)),
)
} returns listOf(folderView).asSuccess()
val foldersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
setupVaultDiskSourceFlows(foldersFlow = foldersFlow)
vaultRepository.foldersStateFlow.test {
// Populate and consume items related to the folders flow
awaitItem()
foldersFlow.tryEmit(listOf(createMockFolder(number = number)))
awaitItem()
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(),
isUpdate = false,
),
)
}
coVerify(exactly = 0) {
folderService.getFolder(any())
vaultDiskSource.saveFolder(any(), any())
}
}
@Test
fun `syncFolderUpsertFlow update with no local folder should do nothing`() = runTest {
val number = 1
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
coEvery {
vaultSdkSource.decryptFolderList(
userId = MOCK_USER_STATE.activeUserId,
folderList = listOf(),
)
} returns listOf<FolderView>().asSuccess()
val foldersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
setupVaultDiskSourceFlows(foldersFlow = foldersFlow)
vaultRepository.foldersStateFlow.test {
// Populate and consume items related to the folders flow
awaitItem()
foldersFlow.tryEmit(listOf())
awaitItem()
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(),
isUpdate = true,
),
)
}
coVerify(exactly = 0) {
folderService.getFolder(any())
vaultDiskSource.saveFolder(any(), any())
}
}
@Test
fun `syncFolderUpsertFlow update with more recent local folder should do nothing`() = runTest {
val number = 1
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
val folderView = createMockFolderView(number = number)
coEvery {
vaultSdkSource.decryptFolderList(
userId = MOCK_USER_STATE.activeUserId,
folderList = listOf(createMockSdkFolder(number = number)),
)
} returns listOf(folderView).asSuccess()
val foldersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
setupVaultDiskSourceFlows(foldersFlow = foldersFlow)
vaultRepository.foldersStateFlow.test {
// Populate and consume items related to the folders flow
awaitItem()
foldersFlow.tryEmit(listOf(createMockFolder(number = number)))
awaitItem()
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.ofInstant(
Instant.ofEpochSecond(0), ZoneId.of("UTC"),
),
isUpdate = true,
),
)
}
coVerify(exactly = 0) {
folderService.getFolder(any())
vaultDiskSource.saveFolder(any(), any())
}
}
@Suppress("MaxLineLength")
@Test
fun `syncFolderUpsertFlow valid create success should make a request for a folder and then store it`() =
runTest {
val number = 1
val userId = MOCK_USER_STATE.activeUserId
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(emptyList())
val folder = mockk<SyncResponseJson.Folder>()
coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess()
coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = false,
),
)
coVerify(exactly = 1) {
folderService.getFolder(folderId = folderId)
vaultDiskSource.saveFolder(userId = userId, folder = folder)
}
}
@Suppress("MaxLineLength")
@Test
fun `syncFolderUpsertFlow valid update success should make a request for a folder and then store it`() =
runTest {
val number = 1
val userId = MOCK_USER_STATE.activeUserId
val folderId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = createMockFolder(
number = number,
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
)
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(listOf(folderView))
val folder = mockk<SyncResponseJson.Folder>()
coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess()
coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
folderId = folderId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = true,
),
)
coVerify(exactly = 1) {
folderService.getFolder(folderId)
vaultDiskSource.saveFolder(userId = userId, folder = folder)
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `exportVaultDataToString should return a success result when data is successfully converted for export`() = fun `exportVaultDataToString should return a success result when data is successfully converted for export`() =