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.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(

View File

@ -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.
*

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.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,

View File

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

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.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`() =