mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
Refactor Folder logic into FolderManager (#5904)
This commit is contained in:
parent
4f244c52fa
commit
d53f3f313c
@ -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
|
||||
}
|
||||
@ -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) }
|
||||
}
|
||||
}
|
||||
@ -5,8 +5,9 @@ import com.bitwarden.core.data.manager.realtime.RealtimeManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
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.FileManager
|
||||
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.SendManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
@ -71,6 +74,24 @@ object VaultManagerModule {
|
||||
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
|
||||
@Singleton
|
||||
fun provideSendManager(
|
||||
|
||||
@ -13,19 +13,17 @@ import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FolderManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
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.GenerateTotpResult
|
||||
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.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.VaultUnlockResult
|
||||
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.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface VaultRepository : CipherManager, SendManager, VaultLockManager {
|
||||
interface VaultRepository : CipherManager, FolderManager, SendManager, VaultLockManager {
|
||||
|
||||
/**
|
||||
* The [VaultFilterType] for the current user.
|
||||
@ -204,21 +202,6 @@ interface VaultRepository : CipherManager, SendManager, VaultLockManager {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
||||
@ -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.updateToPendingOrLoading
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
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.sdk.Fido2CredentialStore
|
||||
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.manager.DatabaseSchemeManager
|
||||
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.observeWhenSubscribedAndUnlocked
|
||||
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.CipherManager
|
||||
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.TotpCodeManager
|
||||
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.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
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.GenerateTotpResult
|
||||
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.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.VaultUnlockResult
|
||||
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.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.toEncryptedSdkCipherList
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@ -116,12 +107,12 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
class VaultRepositoryImpl(
|
||||
private val folderService: FolderService,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val cipherManager: CipherManager,
|
||||
private val folderManager: FolderManager,
|
||||
private val sendManager: SendManager,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val totpCodeManager: TotpCodeManager,
|
||||
@ -133,6 +124,7 @@ class VaultRepositoryImpl(
|
||||
private val credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
) : VaultRepository,
|
||||
CipherManager by cipherManager,
|
||||
FolderManager by folderManager,
|
||||
SendManager by sendManager,
|
||||
VaultLockManager by vaultLockManager {
|
||||
|
||||
@ -281,16 +273,6 @@ class VaultRepositoryImpl(
|
||||
.onEach { sync(forced = false) }
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.syncFolderDeleteFlow
|
||||
.onEach(::deleteFolder)
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.syncFolderUpsertFlow
|
||||
.onEach(::syncFolderIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
|
||||
databaseSchemeManager
|
||||
.databaseSchemeChangeFlow
|
||||
.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(
|
||||
format: ExportFormat,
|
||||
restrictedTypes: List<CipherType>,
|
||||
@ -1049,48 +931,6 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
?: 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(
|
||||
userId: String,
|
||||
forced: Boolean,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.di
|
||||
|
||||
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.platform.datasource.disk.SettingsDiskSource
|
||||
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.manager.CipherManager
|
||||
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.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
@ -33,12 +33,12 @@ object VaultRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVaultRepository(
|
||||
folderService: FolderService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
cipherManager: CipherManager,
|
||||
folderManager: FolderManager,
|
||||
sendManager: SendManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
@ -49,12 +49,12 @@ object VaultRepositoryModule {
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
cipherManager = cipherManager,
|
||||
folderManager = folderManager,
|
||||
sendManager = sendManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
|
||||
@ -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,
|
||||
),
|
||||
)
|
||||
@ -15,22 +15,18 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.network.model.CipherTypeJson
|
||||
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.createMockCollection
|
||||
import com.bitwarden.network.model.createMockDomains
|
||||
import com.bitwarden.network.model.createMockFolder
|
||||
import com.bitwarden.network.model.createMockOrganizationKeys
|
||||
import com.bitwarden.network.model.createMockSend
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.Folder
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.bitwarden.vault.TotpResponse
|
||||
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.manager.DatabaseSchemeManager
|
||||
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.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.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
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.GenerateTotpResult
|
||||
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.UpdateFolderResult
|
||||
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.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.toEncryptedSdkCipherList
|
||||
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.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||
@ -113,7 +103,6 @@ import java.security.GeneralSecurityException
|
||||
import java.security.MessageDigest
|
||||
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
|
||||
@ -131,7 +120,6 @@ class VaultRepositoryTest {
|
||||
private val settingsDiskSource = mockk<SettingsDiskSource> {
|
||||
every { getLastSyncTime(userId = any()) } returns clock.instant()
|
||||
}
|
||||
private val folderService: FolderService = mockk()
|
||||
private val mutableGetCiphersFlow: MutableStateFlow<List<SyncResponseJson.Cipher>> =
|
||||
MutableStateFlow(listOf(createMockCipher(1)))
|
||||
private val vaultDiskSource: VaultDiskSource = mockk {
|
||||
@ -169,18 +157,13 @@ class VaultRepositoryTest {
|
||||
every { databaseSchemeChangeFlow } returns mutableDatabaseSchemeChangeFlow
|
||||
}
|
||||
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableSyncFolderDeleteFlow = bufferedMutableSharedFlow<SyncFolderDeleteData>()
|
||||
private val mutableSyncFolderUpsertFlow = bufferedMutableSharedFlow<SyncFolderUpsertData>()
|
||||
private val pushManager: PushManager = mockk {
|
||||
every { fullSyncFlow } returns mutableFullSyncFlow
|
||||
every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow
|
||||
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
|
||||
}
|
||||
private val vaultSyncManager: VaultSyncManager = mockk()
|
||||
private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk()
|
||||
|
||||
private val vaultRepository = VaultRepositoryImpl(
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
@ -190,6 +173,7 @@ class VaultRepositoryTest {
|
||||
totpCodeManager = totpCodeManager,
|
||||
pushManager = pushManager,
|
||||
cipherManager = mockk(),
|
||||
folderManager = mockk(),
|
||||
sendManager = mockk(),
|
||||
clock = clock,
|
||||
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
|
||||
fun `getAuthCodeFlow with no active user should emit an error`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
@ -2533,210 +2161,6 @@ class VaultRepositoryTest {
|
||||
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")
|
||||
@Test
|
||||
fun `exportVaultDataToString should return a success result when data is successfully converted for export`() =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user