From 3c3ef583b9b5478a1848d56ca6d5cf541898b2b7 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 7 Jun 2024 10:35:48 -0500 Subject: [PATCH] Create manager class to isolate logic for ciphers (#1432) --- .../data/vault/manager/CipherManager.kt | 112 + .../data/vault/manager/CipherManagerImpl.kt | 419 ++++ .../vault/manager/di/VaultManagerModule.kt | 22 + .../data/vault/repository/VaultRepository.kt | 101 +- .../vault/repository/VaultRepositoryImpl.kt | 425 +--- .../repository/di/VaultRepositoryModule.kt | 3 + .../data/vault/manager/CipherManagerTest.kt | 1828 +++++++++++++++++ .../vault/repository/VaultRepositoryTest.kt | 1763 +--------------- 8 files changed, 2392 insertions(+), 2281 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManager.kt new file mode 100644 index 0000000000..867cbd8155 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManager.kt @@ -0,0 +1,112 @@ +package com.x8bit.bitwarden.data.vault.manager + +import android.net.Uri +import com.bitwarden.core.CipherView +import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult + +/** + * Manages the creating, updating, and deleting ciphers and their attachments. + */ +@Suppress("TooManyFunctions") +interface CipherManager { + /** + * Attempt to create a cipher. + */ + suspend fun createCipher( + cipherView: CipherView, + ): CreateCipherResult + + /** + * Attempt to create a cipher that belongs to an organization. + */ + suspend fun createCipherInOrganization( + cipherView: CipherView, + collectionIds: List, + ): CreateCipherResult + + /** + * Attempt to create an attachment for the given [cipherView]. + */ + suspend fun createAttachment( + cipherId: String, + cipherView: CipherView, + fileSizeBytes: String, + fileName: String, + fileUri: Uri, + ): CreateAttachmentResult + + /** + * Attempt to download an attachment file, specified by [attachmentId], for the given + * [cipherView]. + */ + suspend fun downloadAttachment( + cipherView: CipherView, + attachmentId: String, + ): DownloadAttachmentResult + + /** + * Attempt to delete a cipher. + */ + suspend fun hardDeleteCipher( + cipherId: String, + ): DeleteCipherResult + + /** + * Attempt to soft delete a cipher. + */ + suspend fun softDeleteCipher( + cipherId: String, + cipherView: CipherView, + ): DeleteCipherResult + + /** + * Attempt to delete an attachment from a send. + */ + suspend fun deleteCipherAttachment( + cipherId: String, + attachmentId: String, + cipherView: CipherView, + ): DeleteAttachmentResult + + /** + * Attempt to restore a cipher. + */ + suspend fun restoreCipher( + cipherId: String, + cipherView: CipherView, + ): RestoreCipherResult + + /** + * Attempt to share a cipher to the collections with the given collectionIds. + */ + suspend fun shareCipher( + cipherId: String, + organizationId: String, + cipherView: CipherView, + collectionIds: List, + ): ShareCipherResult + + /** + * Attempt to update a cipher. + */ + suspend fun updateCipher( + cipherId: String, + cipherView: CipherView, + ): UpdateCipherResult + + /** + * Attempt to update a cipher with the given collectionIds. + */ + suspend fun updateCipherCollections( + cipherId: String, + cipherView: CipherView, + collectionIds: List, + ): ShareCipherResult +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt new file mode 100644 index 0000000000..be8a98b928 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt @@ -0,0 +1,419 @@ +package com.x8bit.bitwarden.data.vault.manager + +import android.net.Uri +import com.bitwarden.core.AttachmentView +import com.bitwarden.core.CipherView +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.platform.util.flatMap +import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult +import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher +import java.io.File +import java.time.Clock + +/** + * The default implementation of the [CipherManager]. + */ +@Suppress("TooManyFunctions") +class CipherManagerImpl( + private val fileManager: FileManager, + private val authDiskSource: AuthDiskSource, + private val ciphersService: CiphersService, + private val vaultDiskSource: VaultDiskSource, + private val vaultSdkSource: VaultSdkSource, + private val clock: Clock, +) : CipherManager { + private val activeUserId: String? get() = authDiskSource.userState?.activeUserId + + override suspend fun createCipher(cipherView: CipherView): CreateCipherResult { + val userId = activeUserId ?: return CreateCipherResult.Error + return vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView, + ) + .flatMap { ciphersService.createCipher(body = it.toEncryptedNetworkCipher()) } + .onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) } + .fold( + onFailure = { CreateCipherResult.Error }, + onSuccess = { CreateCipherResult.Success }, + ) + } + + override suspend fun createCipherInOrganization( + cipherView: CipherView, + collectionIds: List, + ): CreateCipherResult { + val userId = activeUserId ?: return CreateCipherResult.Error + return vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView, + ) + .flatMap { cipher -> + ciphersService.createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = cipher.toEncryptedNetworkCipher(), + collectionIds = collectionIds, + ), + ) + } + .onSuccess { + vaultDiskSource.saveCipher( + userId = userId, + cipher = it.copy(collectionIds = collectionIds), + ) + } + .fold( + onFailure = { CreateCipherResult.Error }, + onSuccess = { CreateCipherResult.Success }, + ) + } + + override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult { + val userId = activeUserId ?: return DeleteCipherResult.Error + return ciphersService + .hardDeleteCipher(cipherId = cipherId) + .onSuccess { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } + .fold( + onSuccess = { DeleteCipherResult.Success }, + onFailure = { DeleteCipherResult.Error }, + ) + } + + override suspend fun softDeleteCipher( + cipherId: String, + cipherView: CipherView, + ): DeleteCipherResult { + val userId = activeUserId ?: return DeleteCipherResult.Error + return ciphersService + .softDeleteCipher(cipherId = cipherId) + .onSuccess { + vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView.copy(deletedDate = clock.instant()), + ) + .onSuccess { cipher -> + vaultDiskSource.saveCipher( + userId = userId, + cipher = cipher.toEncryptedNetworkCipherResponse(), + ) + } + } + .fold( + onSuccess = { DeleteCipherResult.Success }, + onFailure = { DeleteCipherResult.Error }, + ) + } + + override suspend fun deleteCipherAttachment( + cipherId: String, + attachmentId: String, + cipherView: CipherView, + ): DeleteAttachmentResult { + val userId = activeUserId ?: return DeleteAttachmentResult.Error + return ciphersService + .deleteCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + ) + .flatMap { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = cipherView.copy( + attachments = cipherView.attachments?.mapNotNull { + if (it.id == attachmentId) null else it + }, + ), + ) + } + .onSuccess { cipher -> + vaultDiskSource.saveCipher( + userId = userId, + cipher = cipher.toEncryptedNetworkCipherResponse(), + ) + } + .fold( + onSuccess = { DeleteAttachmentResult.Success }, + onFailure = { DeleteAttachmentResult.Error }, + ) + } + + override suspend fun restoreCipher( + cipherId: String, + cipherView: CipherView, + ): RestoreCipherResult { + val userId = activeUserId ?: return RestoreCipherResult.Error + return ciphersService + .restoreCipher(cipherId = cipherId) + .flatMap { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = cipherView.copy(deletedDate = null), + ) + } + .onSuccess { cipher -> + vaultDiskSource.saveCipher( + userId = userId, + cipher = cipher.toEncryptedNetworkCipherResponse(), + ) + } + .fold( + onSuccess = { RestoreCipherResult.Success }, + onFailure = { RestoreCipherResult.Error }, + ) + } + + override suspend fun updateCipher( + cipherId: String, + cipherView: CipherView, + ): UpdateCipherResult { + val userId = activeUserId ?: return UpdateCipherResult.Error(errorMessage = null) + return vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView, + ) + .flatMap { cipher -> + ciphersService.updateCipher( + cipherId = cipherId, + body = cipher.toEncryptedNetworkCipher(), + ) + } + .map { response -> + when (response) { + is UpdateCipherResponseJson.Invalid -> { + UpdateCipherResult.Error(errorMessage = response.message) + } + + is UpdateCipherResponseJson.Success -> { + vaultDiskSource.saveCipher( + userId = userId, + cipher = response.cipher.copy(collectionIds = cipherView.collectionIds), + ) + UpdateCipherResult.Success + } + } + } + .fold( + onFailure = { UpdateCipherResult.Error(errorMessage = null) }, + onSuccess = { it }, + ) + } + + override suspend fun shareCipher( + cipherId: String, + organizationId: String, + cipherView: CipherView, + collectionIds: List, + ): ShareCipherResult { + val userId = activeUserId ?: return ShareCipherResult.Error + return vaultSdkSource + .moveToOrganization( + userId = userId, + organizationId = organizationId, + cipherView = cipherView, + ) + .flatMap { vaultSdkSource.encryptCipher(userId = userId, cipherView = it) } + .flatMap { cipher -> + ciphersService.shareCipher( + cipherId = cipherId, + body = ShareCipherJsonRequest( + cipher = cipher.toEncryptedNetworkCipher(), + collectionIds = collectionIds, + ), + ) + } + .onSuccess { + vaultDiskSource.saveCipher( + userId = userId, + cipher = it.copy(collectionIds = collectionIds), + ) + } + .fold( + onFailure = { ShareCipherResult.Error }, + onSuccess = { ShareCipherResult.Success }, + ) + } + + override suspend fun updateCipherCollections( + cipherId: String, + cipherView: CipherView, + collectionIds: List, + ): ShareCipherResult { + val userId = activeUserId ?: return ShareCipherResult.Error + return ciphersService + .updateCipherCollections( + cipherId = cipherId, + body = UpdateCipherCollectionsJsonRequest(collectionIds = collectionIds), + ) + .flatMap { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = cipherView.copy(collectionIds = collectionIds), + ) + } + .onSuccess { cipher -> + vaultDiskSource.saveCipher( + userId = userId, + cipher = cipher.toEncryptedNetworkCipherResponse(), + ) + } + .fold( + onSuccess = { ShareCipherResult.Success }, + onFailure = { ShareCipherResult.Error }, + ) + } + + @Suppress("LongMethod") + override suspend fun createAttachment( + cipherId: String, + cipherView: CipherView, + fileSizeBytes: String, + fileName: String, + fileUri: Uri, + ): CreateAttachmentResult { + val userId = activeUserId ?: return CreateAttachmentResult.Error + val attachmentView = AttachmentView( + id = null, + url = null, + size = fileSizeBytes, + sizeName = null, + fileName = fileName, + key = null, + ) + return vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView, + ) + .flatMap { cipher -> + fileManager + .writeUriToCache(fileUri = fileUri) + .flatMap { cacheFile -> + vaultSdkSource + .encryptAttachment( + userId = userId, + cipher = cipher, + attachmentView = attachmentView, + decryptedFilePath = cacheFile.absolutePath, + encryptedFilePath = "${cacheFile.absolutePath}.enc", + ) + .flatMap { attachment -> + ciphersService + .createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + // We know these values are present because + // - the filename/size are passed into the function + // - the SDK call fills in the key + fileName = requireNotNull(attachment.fileName), + key = requireNotNull(attachment.key), + fileSize = requireNotNull(attachment.size), + ), + ) + .flatMap { attachmentJsonResponse -> + val encryptedFile = File("${cacheFile.absolutePath}.enc") + ciphersService + .uploadAttachment( + attachmentJsonResponse = attachmentJsonResponse, + encryptedFile = encryptedFile, + ) + .onSuccess { + fileManager.delete(cacheFile, encryptedFile) + } + .onFailure { + fileManager.delete(cacheFile, encryptedFile) + } + } + } + } + } + .map { it.copy(collectionIds = cipherView.collectionIds) } + .onSuccess { + // Save the send immediately, regardless of whether the decrypt succeeds + vaultDiskSource.saveCipher(userId = userId, cipher = it) + } + .flatMap { + vaultSdkSource.decryptCipher( + userId = userId, + cipher = it.toEncryptedSdkCipher(), + ) + } + .fold( + onFailure = { CreateAttachmentResult.Error }, + onSuccess = { CreateAttachmentResult.Success(cipherView = it) }, + ) + } + + @Suppress("ReturnCount") + override suspend fun downloadAttachment( + cipherView: CipherView, + attachmentId: String, + ): DownloadAttachmentResult { + val userId = activeUserId ?: return DownloadAttachmentResult.Failure + + val cipher = vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView, + ) + .fold( + onSuccess = { it }, + onFailure = { return DownloadAttachmentResult.Failure }, + ) + val attachment = cipher.attachments?.find { it.id == attachmentId } + ?: return DownloadAttachmentResult.Failure + + val attachmentData = ciphersService + .getCipherAttachment( + cipherId = requireNotNull(cipher.id), + attachmentId = attachmentId, + ) + .fold( + onSuccess = { it }, + onFailure = { return DownloadAttachmentResult.Failure }, + ) + + val url = attachmentData.url ?: return DownloadAttachmentResult.Failure + + val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) { + DownloadResult.Failure -> return DownloadAttachmentResult.Failure + is DownloadResult.Success -> result.file + } + + val decryptedFile = File(encryptedFile.path + "_decrypted") + return vaultSdkSource + .decryptFile( + userId = userId, + cipher = cipher, + attachment = attachment, + encryptedFilePath = encryptedFile.path, + decryptedFilePath = decryptedFile.path, + ) + .onSuccess { fileManager.delete(encryptedFile) } + .onFailure { fileManager.delete(encryptedFile) } + .fold( + onSuccess = { DownloadAttachmentResult.Success(file = decryptedFile) }, + onFailure = { DownloadAttachmentResult.Failure }, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt index e17a6fd075..519ea6fc30 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -8,8 +8,12 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository +import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.DownloadService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.CipherManager +import com.x8bit.bitwarden.data.vault.manager.CipherManagerImpl import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager @@ -31,6 +35,24 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object VaultManagerModule { + @Provides + @Singleton + fun provideCipherManager( + ciphersService: CiphersService, + vaultDiskSource: VaultDiskSource, + vaultSdkSource: VaultSdkSource, + authDiskSource: AuthDiskSource, + fileManager: FileManager, + clock: Clock, + ): CipherManager = CipherManagerImpl( + fileManager = fileManager, + authDiskSource = authDiskSource, + ciphersService = ciphersService, + vaultDiskSource = vaultDiskSource, + vaultSdkSource = vaultSdkSource, + clock = clock, + ) + @Provides @Singleton fun provideFileManager( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 20bdd2a2d1..7f5da09af4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -10,26 +10,19 @@ import com.bitwarden.core.SendType import com.bitwarden.core.SendView import com.bitwarden.crypto.Kdf import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem -import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult -import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult -import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult -import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData -import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult 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.RemovePasswordSendResult -import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult import com.x8bit.bitwarden.data.vault.repository.model.SendData -import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult -import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData @@ -42,7 +35,7 @@ import kotlinx.coroutines.flow.StateFlow * Responsible for managing vault data inside the network layer. */ @Suppress("TooManyFunctions") -interface VaultRepository : VaultLockManager { +interface VaultRepository : CipherManager, VaultLockManager { /** * The [VaultFilterType] for the current user. @@ -193,87 +186,6 @@ interface VaultRepository : VaultLockManager { organizationKeys: Map?, ): VaultUnlockResult - /** - * Attempt to create a cipher. - */ - suspend fun createCipher(cipherView: CipherView): CreateCipherResult - - /** - * Attempt to create a cipher that belongs to an organization. - */ - suspend fun createCipherInOrganization( - cipherView: CipherView, - collectionIds: List, - ): CreateCipherResult - - /** - * Attempt to create an attachment for the given [cipherView]. - */ - suspend fun createAttachment( - cipherId: String, - cipherView: CipherView, - fileSizeBytes: String, - fileName: String, - fileUri: Uri, - ): CreateAttachmentResult - - /** - * Attempt to download an attachment file, specified by [attachmentId], for the given - * [cipherView]. - */ - suspend fun downloadAttachment( - cipherView: CipherView, - attachmentId: String, - ): DownloadAttachmentResult - - /** - * Attempt to delete a cipher. - */ - suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult - - /** - * Attempt to soft delete a cipher. - */ - suspend fun softDeleteCipher( - cipherId: String, - cipherView: CipherView, - ): DeleteCipherResult - - /** - * Attempt to restore a cipher. - */ - suspend fun restoreCipher( - cipherId: String, - cipherView: CipherView, - ): RestoreCipherResult - - /** - * Attempt to update a cipher. - */ - suspend fun updateCipher( - cipherId: String, - cipherView: CipherView, - ): UpdateCipherResult - - /** - * Attempt to share a cipher to the collections with the given collectionIds. - */ - suspend fun shareCipher( - cipherId: String, - organizationId: String, - cipherView: CipherView, - collectionIds: List, - ): ShareCipherResult - - /** - * Attempt to update a cipher with the given collectionIds. - */ - suspend fun updateCipherCollections( - cipherId: String, - cipherView: CipherView, - collectionIds: List, - ): ShareCipherResult - /** * Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a * [SendView.type] of [SendType.FILE]. @@ -303,15 +215,6 @@ interface VaultRepository : VaultLockManager { */ suspend fun deleteSend(sendId: String): DeleteSendResult - /** - * Attempt to delete an attachment from a send. - */ - suspend fun deleteCipherAttachment( - cipherId: String, - attachmentId: String, - cipherView: CipherView, - ): DeleteAttachmentResult - /** * Attempt to create a folder. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 8ada887d38..db5ef1ba1e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.vault.repository import android.net.Uri -import com.bitwarden.core.AttachmentView import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView @@ -40,14 +39,9 @@ import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource -import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest -import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateSendJsonResponse -import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson -import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest -import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService @@ -55,37 +49,27 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.FolderService import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager -import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem -import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult -import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult -import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult -import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData -import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult 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.RemovePasswordSendResult -import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult import com.x8bit.bitwarden.data.vault.repository.model.SendData -import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult -import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult import com.x8bit.bitwarden.data.vault.repository.model.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.toDomainsData -import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher -import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher @@ -122,9 +106,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import retrofit2.HttpException -import java.io.File import java.time.Clock -import java.time.Instant import java.time.temporal.ChronoUnit /** @@ -146,6 +128,7 @@ class VaultRepositoryImpl( private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, private val settingsDiskSource: SettingsDiskSource, + private val cipherManager: CipherManager, private val fileManager: FileManager, private val vaultLockManager: VaultLockManager, private val totpCodeManager: TotpCodeManager, @@ -154,6 +137,7 @@ class VaultRepositoryImpl( private val clock: Clock, dispatcherManager: DispatcherManager, ) : VaultRepository, + CipherManager by cipherManager, VaultLockManager by vaultLockManager { private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) @@ -617,409 +601,6 @@ class VaultRepositoryImpl( organizationKeys = organizationKeys, ) - override suspend fun createCipher(cipherView: CipherView): CreateCipherResult { - val userId = activeUserId ?: return CreateCipherResult.Error - return vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView, - ) - .flatMap { cipher -> - ciphersService - .createCipher( - body = cipher.toEncryptedNetworkCipher(), - ) - } - .fold( - onFailure = { - CreateCipherResult.Error - }, - onSuccess = { - vaultDiskSource.saveCipher(userId = userId, cipher = it) - CreateCipherResult.Success - }, - ) - } - - override suspend fun createCipherInOrganization( - cipherView: CipherView, - collectionIds: List, - ): CreateCipherResult { - val userId = activeUserId ?: return CreateCipherResult.Error - return vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView, - ) - .flatMap { cipher -> - ciphersService - .createCipherInOrganization( - body = CreateCipherInOrganizationJsonRequest( - cipher = cipher.toEncryptedNetworkCipher(), - collectionIds = collectionIds, - ), - ) - } - .onSuccess { - vaultDiskSource.saveCipher( - userId = userId, - cipher = it.copy(collectionIds = collectionIds), - ) - } - .fold( - onFailure = { CreateCipherResult.Error }, - onSuccess = { CreateCipherResult.Success }, - ) - } - - override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult { - val userId = activeUserId ?: return DeleteCipherResult.Error - return ciphersService - .hardDeleteCipher(cipherId) - .onSuccess { vaultDiskSource.deleteCipher(userId, cipherId) } - .fold( - onSuccess = { DeleteCipherResult.Success }, - onFailure = { DeleteCipherResult.Error }, - ) - } - - override suspend fun softDeleteCipher( - cipherId: String, - cipherView: CipherView, - ): DeleteCipherResult { - val userId = activeUserId ?: return DeleteCipherResult.Error - return ciphersService - .softDeleteCipher(cipherId) - .fold( - onSuccess = { - vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView.copy( - deletedDate = Instant.now(), - ), - ) - .onSuccess { cipher -> - vaultDiskSource.saveCipher( - userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), - ) - } - DeleteCipherResult.Success - }, - onFailure = { DeleteCipherResult.Error }, - ) - } - - override suspend fun deleteCipherAttachment( - cipherId: String, - attachmentId: String, - cipherView: CipherView, - ): DeleteAttachmentResult { - val userId = activeUserId ?: return DeleteAttachmentResult.Error - return ciphersService - .deleteCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - ) - .flatMap { - vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView.copy( - attachments = cipherView.attachments?.mapNotNull { - if (it.id == attachmentId) null else it - }, - ), - ) - } - .onSuccess { cipher -> - vaultDiskSource.saveCipher( - userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), - ) - } - .fold( - onSuccess = { DeleteAttachmentResult.Success }, - onFailure = { DeleteAttachmentResult.Error }, - ) - } - - override suspend fun restoreCipher( - cipherId: String, - cipherView: CipherView, - ): RestoreCipherResult { - val userId = activeUserId ?: return RestoreCipherResult.Error - return ciphersService - .restoreCipher(cipherId) - .flatMap { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = cipherView.copy( - deletedDate = null, - ), - ) - } - .onSuccess { cipher -> - vaultDiskSource.saveCipher( - userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), - ) - } - .fold( - onSuccess = { RestoreCipherResult.Success }, - onFailure = { RestoreCipherResult.Error }, - ) - } - - override suspend fun updateCipher( - cipherId: String, - cipherView: CipherView, - ): UpdateCipherResult { - val userId = activeUserId ?: return UpdateCipherResult.Error(null) - return vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView, - ) - .flatMap { cipher -> - ciphersService.updateCipher( - cipherId = cipherId, - body = cipher.toEncryptedNetworkCipher(), - ) - } - .fold( - onFailure = { UpdateCipherResult.Error(errorMessage = null) }, - onSuccess = { response -> - when (response) { - is UpdateCipherResponseJson.Invalid -> { - UpdateCipherResult.Error(errorMessage = response.message) - } - - is UpdateCipherResponseJson.Success -> { - vaultDiskSource.saveCipher( - userId = userId, - cipher = response.cipher - .copy(collectionIds = cipherView.collectionIds), - ) - UpdateCipherResult.Success - } - } - }, - ) - } - - override suspend fun shareCipher( - cipherId: String, - organizationId: String, - cipherView: CipherView, - collectionIds: List, - ): ShareCipherResult { - val userId = activeUserId ?: return ShareCipherResult.Error - return vaultSdkSource - .moveToOrganization( - userId = userId, - organizationId = organizationId, - cipherView = cipherView, - ) - .flatMap { vaultSdkSource.encryptCipher(userId = userId, cipherView = it) } - .flatMap { cipher -> - ciphersService.shareCipher( - cipherId = cipherId, - body = ShareCipherJsonRequest( - cipher = cipher.toEncryptedNetworkCipher(), - collectionIds = collectionIds, - ), - ) - } - .onSuccess { - vaultDiskSource.saveCipher( - userId = userId, - cipher = it.copy(collectionIds = collectionIds), - ) - } - .fold( - onFailure = { ShareCipherResult.Error }, - onSuccess = { ShareCipherResult.Success }, - ) - } - - override suspend fun updateCipherCollections( - cipherId: String, - cipherView: CipherView, - collectionIds: List, - ): ShareCipherResult { - val userId = activeUserId ?: return ShareCipherResult.Error - return ciphersService - .updateCipherCollections( - cipherId = cipherId, - body = UpdateCipherCollectionsJsonRequest(collectionIds = collectionIds), - ) - .flatMap { - vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView.copy(collectionIds = collectionIds), - ) - } - .onSuccess { cipher -> - vaultDiskSource.saveCipher( - userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), - ) - } - .fold( - onSuccess = { ShareCipherResult.Success }, - onFailure = { ShareCipherResult.Error }, - ) - } - - @Suppress("LongMethod") - override suspend fun createAttachment( - cipherId: String, - cipherView: CipherView, - fileSizeBytes: String, - fileName: String, - fileUri: Uri, - ): CreateAttachmentResult { - val userId = activeUserId ?: return CreateAttachmentResult.Error - val attachmentView = AttachmentView( - id = null, - url = null, - size = fileSizeBytes, - sizeName = null, - fileName = fileName, - key = null, - ) - return vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView, - ) - .flatMap { cipher -> - fileManager - .writeUriToCache(fileUri = fileUri) - .flatMap { cacheFile -> - vaultSdkSource - .encryptAttachment( - userId = userId, - cipher = cipher, - attachmentView = attachmentView, - decryptedFilePath = cacheFile.absolutePath, - encryptedFilePath = "${cacheFile.absolutePath}.enc", - ) - .flatMap { attachment -> - ciphersService - .createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - // We know these values are present because - // - the filename/size are passed into the function - // - the SDK call fills in the key - fileName = requireNotNull(attachment.fileName), - key = requireNotNull(attachment.key), - fileSize = requireNotNull(attachment.size), - ), - ) - .flatMap { attachmentJsonResponse -> - val encryptedFile = File("${cacheFile.absolutePath}.enc") - ciphersService - .uploadAttachment( - attachmentJsonResponse = attachmentJsonResponse, - encryptedFile = encryptedFile, - ) - .onSuccess { - fileManager - .delete( - cacheFile, - encryptedFile, - ) - } - .onFailure { - fileManager - .delete( - cacheFile, - encryptedFile, - ) - } - } - } - } - } - .map { it.copy(collectionIds = cipherView.collectionIds) } - .onSuccess { - // Save the send immediately, regardless of whether the decrypt succeeds - vaultDiskSource.saveCipher(userId = userId, cipher = it) - } - .flatMap { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = it.toEncryptedSdkCipher(), - ) - } - .fold( - onFailure = { CreateAttachmentResult.Error }, - onSuccess = { CreateAttachmentResult.Success(it) }, - ) - } - - @Suppress("ReturnCount") - override suspend fun downloadAttachment( - cipherView: CipherView, - attachmentId: String, - ): DownloadAttachmentResult { - val userId = requireNotNull(authDiskSource.userState?.activeUserId) - - val cipher = vaultSdkSource - .encryptCipher( - userId = userId, - cipherView = cipherView, - ) - .fold( - onSuccess = { it }, - onFailure = { return DownloadAttachmentResult.Failure }, - ) - val attachment = cipher.attachments?.find { it.id == attachmentId } - ?: return DownloadAttachmentResult.Failure - - val attachmentData = ciphersService - .getCipherAttachment( - cipherId = requireNotNull(cipher.id), - attachmentId = attachmentId, - ) - .fold( - onSuccess = { it }, - onFailure = { return DownloadAttachmentResult.Failure }, - ) - - val url = attachmentData.url ?: return DownloadAttachmentResult.Failure - - val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) { - DownloadResult.Failure -> return DownloadAttachmentResult.Failure - is DownloadResult.Success -> result.file - } - - val decryptedFile = File(encryptedFile.path + "_decrypted") - return vaultSdkSource - .decryptFile( - userId = userId, - cipher = cipher, - attachment = attachment, - encryptedFilePath = encryptedFile.path, - decryptedFilePath = decryptedFile.path, - ) - .fold( - onSuccess = { - encryptedFile.delete() - DownloadAttachmentResult.Success(decryptedFile) - }, - onFailure = { - encryptedFile.delete() - DownloadAttachmentResult.Failure - }, - ) - } - @Suppress("ReturnCount") override suspend fun createSend( sendView: SendView, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index d096dfd875..39486a5a66 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.FolderService import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager @@ -41,6 +42,7 @@ object VaultRepositoryModule { vaultSdkSource: VaultSdkSource, authDiskSource: AuthDiskSource, settingsDiskSource: SettingsDiskSource, + cipherManager: CipherManager, fileManager: FileManager, vaultLockManager: VaultLockManager, dispatcherManager: DispatcherManager, @@ -57,6 +59,7 @@ object VaultRepositoryModule { vaultSdkSource = vaultSdkSource, authDiskSource = authDiskSource, settingsDiskSource = settingsDiskSource, + cipherManager = cipherManager, fileManager = fileManager, vaultLockManager = vaultLockManager, dispatcherManager = dispatcherManager, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt new file mode 100644 index 0000000000..35ddb95061 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt @@ -0,0 +1,1828 @@ +package com.x8bit.bitwarden.data.vault.manager + +import android.net.Uri +import com.bitwarden.core.Attachment +import com.bitwarden.core.Cipher +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.util.asFailure +import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachmentJsonResponse +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAttachmentView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher +import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult +import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult +import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.unmockkStatic +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.io.File +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset + +@Suppress("LargeClass") +class CipherManagerTest { + + private val clock: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, + ) + private val fileManager: FileManager = mockk { + coEvery { delete(*anyVararg()) } just runs + } + private val fakeAuthDiskSource = FakeAuthDiskSource() + private val ciphersService: CiphersService = mockk() + private val vaultDiskSource: VaultDiskSource = mockk() + private val vaultSdkSource: VaultSdkSource = mockk() + + private val cipherManager: CipherManagerImpl = CipherManagerImpl( + ciphersService = ciphersService, + vaultDiskSource = vaultDiskSource, + vaultSdkSource = vaultSdkSource, + authDiskSource = fakeAuthDiskSource, + fileManager = fileManager, + clock = clock, + ) + + @BeforeEach + fun setup() { + mockkStatic(Uri::class) + } + + @AfterEach + fun tearDown() { + unmockkStatic(Uri::class, Instant::class) + unmockkStatic(Cipher::toEncryptedNetworkCipherResponse) + } + + @Test + fun `createCipher with no active user should return CreateCipherResult failure`() = runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.createCipher(cipherView = mockk()) + + assertEquals(CreateCipherResult.Error, result) + } + + @Test + fun `createCipher with encryptCipher failure should return CreateCipherResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns IllegalStateException().asFailure() + + val result = cipherManager.createCipher(cipherView = mockCipherView) + + assertEquals(CreateCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipher with ciphersService createCipher failure should return CreateCipherResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.createCipher( + body = createMockCipherJsonRequest(number = 1, hasNullUri = true), + ) + } returns IllegalStateException().asFailure() + + val result = cipherManager.createCipher(cipherView = mockCipherView) + + assertEquals(CreateCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipher with ciphersService createCipher success should return CreateCipherResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + val mockCipher = createMockCipher(number = 1) + coEvery { + ciphersService.createCipher( + body = createMockCipherJsonRequest(number = 1, hasNullUri = true), + ) + } returns mockCipher.asSuccess() + coEvery { vaultDiskSource.saveCipher(userId, mockCipher) } just runs + + val result = cipherManager.createCipher(cipherView = mockCipherView) + + assertEquals(CreateCipherResult.Success, result) + } + + @Test + fun `createCipherInOrganization with no active user should return CreateCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.createCipherInOrganization( + cipherView = mockk(), + collectionIds = mockk(), + ) + + assertEquals(CreateCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipherInOrganization with encryptCipher failure should return CreateCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns IllegalStateException().asFailure() + + val result = cipherManager.createCipherInOrganization( + cipherView = mockCipherView, + collectionIds = mockk(), + ) + + assertEquals(CreateCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipherInOrganization with ciphersService createCipher failure should return CreateCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), + collectionIds = listOf("mockId-1"), + ), + ) + } returns IllegalStateException().asFailure() + + val result = cipherManager.createCipherInOrganization( + cipherView = mockCipherView, + collectionIds = listOf("mockId-1"), + ) + + assertEquals(CreateCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipherInOrganization with ciphersService createCipher success should return CreateCipherResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + val mockCipher = createMockCipher(number = 1) + coEvery { + ciphersService.createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), + collectionIds = listOf("mockId-1"), + ), + ) + } returns mockCipher.asSuccess() + coEvery { + vaultDiskSource.saveCipher( + userId, + mockCipher.copy(collectionIds = listOf("mockId-1")), + ) + } just runs + + val result = cipherManager.createCipherInOrganization( + cipherView = mockCipherView, + collectionIds = listOf("mockId-1"), + ) + + assertEquals(CreateCipherResult.Success, result) + } + + @Test + fun `updateCipher with no active user should return UpdateCipherResult Error`() = runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.updateCipher( + cipherId = "cipherId", + cipherView = mockk(), + ) + + assertEquals(UpdateCipherResult.Error(errorMessage = null), result) + } + + @Test + fun `updateCipher with encryptCipher failure should return UpdateCipherResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId1234" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns IllegalStateException().asFailure() + + val result = cipherManager.updateCipher( + cipherId = cipherId, + cipherView = mockCipherView, + ) + + assertEquals(UpdateCipherResult.Error(errorMessage = null), result) + } + + @Test + @Suppress("MaxLineLength") + fun `updateCipher with ciphersService updateCipher failure should return UpdateCipherResult Error with a null message`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId1234" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.updateCipher( + cipherId = cipherId, + body = createMockCipherJsonRequest(number = 1, hasNullUri = true), + ) + } returns IllegalStateException().asFailure() + + val result = cipherManager.updateCipher( + cipherId = cipherId, + cipherView = mockCipherView, + ) + + assertEquals(UpdateCipherResult.Error(errorMessage = null), result) + } + + @Test + @Suppress("MaxLineLength") + fun `updateCipher with ciphersService updateCipher Invalid response should return UpdateCipherResult Error with a non-null message`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId1234" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.updateCipher( + cipherId = cipherId, + body = createMockCipherJsonRequest(number = 1, hasNullUri = true), + ) + } returns UpdateCipherResponseJson + .Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ) + .asSuccess() + + val result = cipherManager.updateCipher( + cipherId = cipherId, + cipherView = mockCipherView, + ) + + assertEquals( + UpdateCipherResult.Error(errorMessage = "You do not have permission to edit this."), + result, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `updateCipher with ciphersService updateCipher Success response should return UpdateCipherResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId1234" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + val mockCipher = createMockCipher(number = 1) + coEvery { + ciphersService.updateCipher( + cipherId = cipherId, + body = createMockCipherJsonRequest(number = 1, hasNullUri = true), + ) + } returns UpdateCipherResponseJson + .Success(cipher = mockCipher) + .asSuccess() + coEvery { + vaultDiskSource.saveCipher( + userId = userId, + cipher = mockCipher.copy(collectionIds = mockCipherView.collectionIds), + ) + } just runs + + val result = cipherManager.updateCipher( + cipherId = cipherId, + cipherView = mockCipherView, + ) + + assertEquals(UpdateCipherResult.Success, result) + } + + @Test + fun `hardDeleteCipher with no active user should return DeleteCipherResult Error`() = runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.hardDeleteCipher( + cipherId = "cipherId", + ) + + assertEquals(DeleteCipherResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `hardDeleteCipher with ciphersService hardDeleteCipher failure should return DeleteCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipherId = "mockId-1" + coEvery { + ciphersService.hardDeleteCipher(cipherId = cipherId) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.hardDeleteCipher(cipherId) + + assertEquals(DeleteCipherResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `hardDeleteCipher with ciphersService hardDeleteCipher success should return DeleteCipherResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "mockId-1" + coEvery { ciphersService.hardDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() + coEvery { vaultDiskSource.deleteCipher(userId, cipherId) } just runs + + val result = cipherManager.hardDeleteCipher(cipherId) + + assertEquals(DeleteCipherResult.Success, result) + } + + @Test + fun `softDeleteCipher with no active user should return DeleteCipherResult Error`() = runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.softDeleteCipher( + cipherId = "cipherId", + cipherView = mockk(), + ) + + assertEquals(DeleteCipherResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `softDeleteCipher with ciphersService softDeleteCipher failure should return DeleteCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipherId = "mockId-1" + coEvery { + ciphersService.softDeleteCipher(cipherId = cipherId) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.softDeleteCipher( + cipherId = cipherId, + cipherView = createMockCipherView(number = 1), + ) + + assertEquals(DeleteCipherResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `softDeleteCipher with ciphersService softDeleteCipher success should return DeleteCipherResult success`() = + runTest { + mockkStatic(Cipher::toEncryptedNetworkCipherResponse) + every { + createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() + } returns createMockCipher(number = 1) + val fixedInstant = Instant.parse("2023-10-27T12:00:00Z") + val userId = "mockId-1" + val cipherId = "mockId-1" + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = createMockCipherView(number = 1).copy(deletedDate = fixedInstant), + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { ciphersService.softDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() + coEvery { + vaultDiskSource.saveCipher( + userId = userId, + cipher = createMockCipher(number = 1), + ) + } just runs + val cipherView = createMockCipherView(number = 1) + mockkStatic(Instant::class) + every { Instant.now() } returns fixedInstant + + val result = cipherManager.softDeleteCipher( + cipherId = cipherId, + cipherView = cipherView, + ) + + assertEquals(DeleteCipherResult.Success, result) + unmockkStatic(Instant::class) + unmockkStatic(Cipher::toEncryptedNetworkCipherResponse) + } + + @Test + fun `deleteCipherAttachment with no active user should return DeleteAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.deleteCipherAttachment( + cipherId = "cipherId", + attachmentId = "attachmentId", + cipherView = mockk(), + ) + + assertEquals(DeleteAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `deleteCipherAttachment with ciphersService deleteCipherAttachment failure should return DeleteAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipherId = "mockId-1" + val attachmentId = "mockId-1" + coEvery { + ciphersService.deleteCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + ) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.deleteCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + cipherView = createMockCipherView(number = 1), + ) + + assertEquals(DeleteAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `deleteCipherAttachment with ciphersService deleteCipherAttachment success should return DeleteAttachmentResult success`() = + runTest { + mockkStatic(Cipher::toEncryptedNetworkCipherResponse) + every { + createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() + } returns createMockCipher(number = 1) + val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") + val userId = "mockId-1" + val cipherId = "mockId-1" + val attachmentId = "mockId-1" + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = createMockCipherView(number = 1).copy(attachments = emptyList()), + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { + ciphersService.deleteCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + ) + } returns Unit.asSuccess() + coEvery { + vaultDiskSource.saveCipher( + userId = userId, + cipher = createMockCipher(number = 1), + ) + } just runs + val cipherView = createMockCipherView(number = 1) + mockkStatic(Instant::class) + every { Instant.now() } returns fixedInstant + + val result = cipherManager.deleteCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + cipherView = cipherView, + ) + + assertEquals(DeleteAttachmentResult.Success, result) + unmockkStatic(Instant::class) + unmockkStatic(Cipher::toEncryptedNetworkCipherResponse) + } + + @Test + fun `restoreCipher with no active user should return RestoreCipherResult Error`() = runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.restoreCipher( + cipherId = "cipherId", + cipherView = mockk(), + ) + + assertEquals(RestoreCipherResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `restoreCipher with ciphersService restoreCipher failure should return RestoreCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipherId = "mockId-1" + coEvery { + ciphersService.restoreCipher(cipherId = cipherId) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.restoreCipher( + cipherId = cipherId, + cipherView = createMockCipherView(number = 1), + ) + + assertEquals(RestoreCipherResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `restoreCipher with ciphersService restoreCipher success should return RestoreCipherResult success`() = + runTest { + mockkStatic(Cipher::toEncryptedNetworkCipherResponse) + every { + createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() + } returns createMockCipher(number = 1) + val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") + val userId = "mockId-1" + val cipherId = "mockId-1" + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = createMockCipherView(number = 1).copy(deletedDate = null), + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { ciphersService.restoreCipher(cipherId = cipherId) } returns Unit.asSuccess() + coEvery { + vaultDiskSource.saveCipher( + userId = userId, + cipher = createMockCipher(number = 1), + ) + } just runs + val cipherView = createMockCipherView(number = 1) + mockkStatic(Instant::class) + every { Instant.now() } returns fixedInstant + + val result = cipherManager.restoreCipher( + cipherId = cipherId, + cipherView = cipherView, + ) + + assertEquals(RestoreCipherResult.Success, result) + } + + @Test + fun `shareCipher with no active user should return ShareCipherResult Error`() = runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.shareCipher( + cipherId = "cipherId", + organizationId = "organizationId", + cipherView = mockk(), + collectionIds = emptyList(), + ) + + assertEquals(ShareCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `shareCipher with cipherService shareCipher success should return ShareCipherResultSuccess`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val organizationId = "organizationId" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.moveToOrganization( + userId = userId, + organizationId = organizationId, + cipherView = createMockCipherView(number = 1), + ) + } returns mockCipherView.asSuccess() + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.shareCipher( + cipherId = "mockId-1", + body = ShareCipherJsonRequest( + cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), + collectionIds = listOf("mockId-1"), + ), + ) + } returns createMockCipher(number = 1).asSuccess() + coEvery { + vaultDiskSource.saveCipher( + userId = userId, + cipher = createMockCipher(number = 1).copy(collectionIds = listOf("mockId-1")), + ) + } just runs + + val result = cipherManager.shareCipher( + cipherId = "mockId-1", + organizationId = organizationId, + cipherView = createMockCipherView(number = 1), + collectionIds = listOf("mockId-1"), + ) + + assertEquals(ShareCipherResult.Success, result) + } + + @Test + @Suppress("MaxLineLength") + fun `shareCipher with cipherService shareCipher failure should return ShareCipherResultError`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val organizationId = "organizationId" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.moveToOrganization( + userId = userId, + organizationId = organizationId, + cipherView = createMockCipherView(number = 1), + ) + } returns mockCipherView.asSuccess() + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.shareCipher( + cipherId = "mockId-1", + body = ShareCipherJsonRequest( + cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), + collectionIds = listOf("mockId-1"), + ), + ) + } returns Throwable("Fail").asFailure() + coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs + + val result = cipherManager.shareCipher( + cipherId = "mockId-1", + organizationId = organizationId, + cipherView = createMockCipherView(number = 1), + collectionIds = listOf("mockId-1"), + ) + + assertEquals(ShareCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `shareCipher with cipherService encryptCipher failure should return ShareCipherResultError`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val organizationId = "organizationId" + coEvery { + vaultSdkSource.moveToOrganization( + userId = userId, + organizationId = organizationId, + cipherView = createMockCipherView(number = 1), + ) + } returns Throwable("Fail").asFailure() + coEvery { + ciphersService.shareCipher( + cipherId = "mockId-1", + body = ShareCipherJsonRequest( + cipher = createMockCipherJsonRequest(number = 1), + collectionIds = listOf("mockId-1"), + ), + ) + } returns createMockCipher(number = 1).asSuccess() + coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs + + val result = cipherManager.shareCipher( + cipherId = "mockId-1", + organizationId = organizationId, + cipherView = createMockCipherView(number = 1), + collectionIds = listOf("mockId-1"), + ) + + assertEquals(ShareCipherResult.Error, result) + } + + @Test + fun `updateCipherCollections with no active user should return ShareCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.updateCipherCollections( + cipherId = "cipherId", + cipherView = mockk(), + collectionIds = emptyList(), + ) + + assertEquals(ShareCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `updateCipherCollections with cipherService updateCipherCollections success should return ShareCipherResultSuccess`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = createMockCipherView(number = 1), + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.updateCipherCollections( + cipherId = "mockId-1", + body = UpdateCipherCollectionsJsonRequest( + collectionIds = listOf("mockId-1"), + ), + ) + } returns Unit.asSuccess() + coEvery { vaultDiskSource.saveCipher(userId, any()) } just runs + + val result = cipherManager.updateCipherCollections( + cipherId = "mockId-1", + cipherView = createMockCipherView(number = 1).copy( + collectionIds = listOf("mockId-1"), + ), + collectionIds = listOf("mockId-1"), + ) + + assertEquals(ShareCipherResult.Success, result) + } + + @Test + @Suppress("MaxLineLength") + fun `updateCipherCollections with updateCipherCollections shareCipher failure should return ShareCipherResultError`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = createMockCipherView(number = 1), + ) + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + coEvery { + ciphersService.updateCipherCollections( + cipherId = "mockId-1", + body = UpdateCipherCollectionsJsonRequest( + collectionIds = listOf("mockId-1"), + ), + ) + } returns Throwable("Fail").asFailure() + coEvery { vaultDiskSource.saveCipher(userId, any()) } just runs + + val result = cipherManager.updateCipherCollections( + cipherId = "mockId-1", + cipherView = createMockCipherView(number = 1).copy( + collectionIds = listOf("mockId-1"), + ), + collectionIds = listOf("mockId-1"), + ) + + assertEquals(ShareCipherResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `updateCipherCollections with updateCipherCollections encryptCipher failure should return ShareCipherResultError`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = createMockCipherView(number = 1), + ) + } returns Throwable("Fail").asFailure() + coEvery { + ciphersService.updateCipherCollections( + cipherId = "mockId-1", + body = UpdateCipherCollectionsJsonRequest( + collectionIds = listOf("mockId-1"), + ), + ) + } returns Unit.asSuccess() + coEvery { vaultDiskSource.saveCipher(userId, any()) } just runs + + val result = cipherManager.updateCipherCollections( + cipherId = "mockId-1", + cipherView = createMockCipherView(number = 1).copy( + collectionIds = listOf("mockId-1"), + ), + collectionIds = listOf("mockId-1"), + ) + + assertEquals(ShareCipherResult.Error, result) + } + + @Test + fun `createAttachment with no active user should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = cipherManager.createAttachment( + cipherId = "cipherId", + cipherView = mockk(), + fileSizeBytes = "mockFileSize", + fileName = "mockFileName", + fileUri = mockk(), + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with encryptCipher failure should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with encryptAttachment failure should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFile = File.createTempFile("mockFile", "temp") + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with uriToByteArray failure should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with createAttachment failure should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns mockAttachment.asSuccess() + coEvery { + ciphersService.createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + fileName = mockFileName, + key = "mockKey-1", + fileSize = mockFileSize, + ), + ) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with uploadAttachment failure should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) + val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns mockAttachment.asSuccess() + coEvery { + ciphersService.createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + fileName = mockFileName, + key = "mockKey-1", + fileSize = mockFileSize, + ), + ) + } returns mockAttachmentJsonResponse.asSuccess() + coEvery { + ciphersService.uploadAttachment( + attachmentJsonResponse = mockAttachmentJsonResponse, + encryptedFile = File("${mockFile.absoluteFile}.enc"), + ) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with decryptCipher failure should return CreateAttachmentResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) + val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) + val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) + val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( + collectionIds = listOf("mockId-1"), + ) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns mockAttachment.asSuccess() + coEvery { + ciphersService.createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + fileName = mockFileName, + key = "mockKey-1", + fileSize = mockFileSize, + ), + ) + } returns mockAttachmentJsonResponse.asSuccess() + coEvery { + ciphersService.uploadAttachment( + attachmentJsonResponse = mockAttachmentJsonResponse, + encryptedFile = File("${mockFile.absoluteFile}.enc"), + ) + } returns mockCipherResponse.asSuccess() + coEvery { + vaultDiskSource.saveCipher(userId = userId, cipher = mockUpdatedCipherResponse) + } just runs + coEvery { + vaultSdkSource.decryptCipher( + userId = userId, + cipher = mockUpdatedCipherResponse.toEncryptedSdkCipher(), + ) + } returns Throwable("Fail").asFailure() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `createAttachment with createAttachment success should return CreateAttachmentResult Success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) + val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) + val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) + val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( + collectionIds = listOf("mockId-1"), + ) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns mockAttachment.asSuccess() + coEvery { + ciphersService.createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + fileName = mockFileName, + key = "mockKey-1", + fileSize = mockFileSize, + ), + ) + } returns mockAttachmentJsonResponse.asSuccess() + coEvery { + ciphersService.uploadAttachment( + attachmentJsonResponse = mockAttachmentJsonResponse, + encryptedFile = File("${mockFile.absolutePath}.enc"), + ) + } returns mockCipherResponse.asSuccess() + coEvery { + vaultDiskSource.saveCipher(userId = userId, cipher = mockUpdatedCipherResponse) + } just runs + coEvery { + vaultSdkSource.decryptCipher( + userId = userId, + cipher = mockUpdatedCipherResponse.toEncryptedSdkCipher(), + ) + } returns mockCipherView.asSuccess() + + val result = cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + assertEquals(CreateAttachmentResult.Success(cipherView = mockCipherView), result) + } + + @Test + fun `createAttachment should delete temp files after upload success`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) + val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) + val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) + val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( + collectionIds = listOf("mockId-1"), + ) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns mockAttachment.asSuccess() + coEvery { + ciphersService.createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + fileName = mockFileName, + key = "mockKey-1", + fileSize = mockFileSize, + ), + ) + } returns mockAttachmentJsonResponse.asSuccess() + coEvery { + ciphersService.uploadAttachment( + attachmentJsonResponse = mockAttachmentJsonResponse, + encryptedFile = File("${mockFile.absolutePath}.enc"), + ) + } returns mockCipherResponse.asSuccess() + coEvery { + vaultDiskSource.saveCipher(userId = userId, cipher = mockUpdatedCipherResponse) + } just runs + coEvery { + vaultSdkSource.decryptCipher( + userId = userId, + cipher = mockUpdatedCipherResponse.toEncryptedSdkCipher(), + ) + } returns mockCipherView.asSuccess() + + cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + coVerify(exactly = 1) { + fileManager.delete(*anyVararg()) + } + } + + @Test + fun `createAttachment should delete temp files after upload failure`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "cipherId-1" + val mockUri = setupMockUri(url = "www.test.com") + val mockCipherView = createMockCipherView(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockFileName = "mockFileName-1" + val mockFileSize = "1" + val mockAttachmentView = createMockAttachmentView(number = 1).copy( + sizeName = null, + id = null, + url = null, + key = null, + ) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) + val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) + coEvery { + vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) + } returns mockCipher.asSuccess() + coEvery { + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() + coEvery { + vaultSdkSource.encryptAttachment( + userId = userId, + cipher = mockCipher, + attachmentView = mockAttachmentView, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", + ) + } returns mockAttachment.asSuccess() + coEvery { + ciphersService.createAttachment( + cipherId = cipherId, + body = AttachmentJsonRequest( + fileName = mockFileName, + key = "mockKey-1", + fileSize = mockFileSize, + ), + ) + } returns mockAttachmentJsonResponse.asSuccess() + coEvery { + ciphersService.uploadAttachment( + attachmentJsonResponse = mockAttachmentJsonResponse, + encryptedFile = File("${mockFile.absolutePath}.enc"), + ) + } returns Throwable("Fail").asFailure() + + cipherManager.createAttachment( + cipherId = cipherId, + cipherView = mockCipherView, + fileSizeBytes = mockFileSize, + fileName = mockFileName, + fileUri = mockUri, + ) + + coVerify(exactly = 1) { + fileManager.delete(*anyVararg()) + } + } + + @Test + fun `downloadAttachment with missing attachment should return Failure`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val cipher = mockk { + every { attachments } returns emptyList() + every { id } returns "mockId-1" + } + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns cipher.asSuccess() + + assertEquals( + DownloadAttachmentResult.Failure, + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } + coVerify(exactly = 0) { + ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any()) + } + } + + @Test + fun `downloadAttachment with failed attachment details request should return Failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val attachment = mockk { + every { id } returns attachmentId + } + val cipher = mockk { + every { attachments } returns listOf(attachment) + every { id } returns "mockId-1" + } + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns cipher.asSuccess() + + coEvery { + ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any()) + } returns Throwable().asFailure() + + assertEquals( + DownloadAttachmentResult.Failure, + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + ciphersService.getCipherAttachment( + cipherId = requireNotNull(cipherView.id), + attachmentId = attachmentId, + ) + } + } + + @Test + fun `downloadAttachment with attachment details missing url should return Failure`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val attachment = mockk { + every { id } returns attachmentId + } + val cipher = mockk { + every { attachments } returns listOf(attachment) + every { id } returns "mockId-1" + } + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns cipher.asSuccess() + + val response = mockk { + every { url } returns null + } + coEvery { + ciphersService.getCipherAttachment(any(), any()) + } returns response.asSuccess() + + assertEquals( + DownloadAttachmentResult.Failure, + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + ciphersService.getCipherAttachment( + cipherId = requireNotNull(cipherView.id), + attachmentId = attachmentId, + ) + } + } + + @Test + fun `downloadAttachment with failed download should return Failure`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val attachment = mockk { + every { id } returns attachmentId + } + val cipher = mockk { + every { attachments } returns listOf(attachment) + every { id } returns "mockId-1" + } + + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns cipher.asSuccess() + + val response = mockk { + every { url } returns "https://bitwarden.com" + } + coEvery { + ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any()) + } returns response.asSuccess() + + coEvery { + fileManager.downloadFileToCache(url = any()) + } returns DownloadResult.Failure + + assertEquals( + DownloadAttachmentResult.Failure, + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + ciphersService.getCipherAttachment( + cipherId = requireNotNull(cipherView.id), + attachmentId = attachmentId, + ) + fileManager.downloadFileToCache("https://bitwarden.com") + } + } + + @Test + fun `downloadAttachment with failed decryption should delete file and return Failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val attachment = mockk { + every { id } returns attachmentId + } + val cipher = mockk { + every { attachments } returns listOf(attachment) + every { id } returns "mockId-1" + } + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns cipher.asSuccess() + + val response = mockk { + every { url } returns "https://bitwarden.com" + } + coEvery { + ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any()) + } returns response.asSuccess() + + val file = mockk { + every { path } returns "path/to/encrypted/file" + } + coEvery { fileManager.delete(file) } just runs + coEvery { + fileManager.downloadFileToCache(url = any()) + } returns DownloadResult.Success(file) + + coEvery { + vaultSdkSource.decryptFile( + userId = MOCK_USER_STATE.activeUserId, + cipher = cipher, + attachment = attachment, + encryptedFilePath = "path/to/encrypted/file", + decryptedFilePath = "path/to/encrypted/file_decrypted", + ) + } returns Throwable().asFailure() + + assertEquals( + DownloadAttachmentResult.Failure, + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + ciphersService.getCipherAttachment( + cipherId = requireNotNull(cipherView.id), + attachmentId = attachmentId, + ) + fileManager.downloadFileToCache("https://bitwarden.com") + vaultSdkSource.decryptFile( + userId = MOCK_USER_STATE.activeUserId, + cipher = cipher, + attachment = attachment, + encryptedFilePath = "path/to/encrypted/file", + decryptedFilePath = "path/to/encrypted/file_decrypted", + ) + fileManager.delete(file) + } + } + + @Test + fun `downloadAttachment with successful decryption should delete file and return Success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val attachment = mockk { + every { id } returns attachmentId + } + val cipher = mockk { + every { attachments } returns listOf(attachment) + every { id } returns "mockId-1" + } + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns cipher.asSuccess() + + val response = mockk { + every { url } returns "https://bitwarden.com" + } + coEvery { + ciphersService.getCipherAttachment(any(), any()) + } returns response.asSuccess() + + val file = mockk { + every { path } returns "path/to/encrypted/file" + } + coEvery { fileManager.delete(file) } just runs + coEvery { + fileManager.downloadFileToCache(any()) + } returns DownloadResult.Success(file) + + coEvery { + vaultSdkSource.decryptFile( + userId = MOCK_USER_STATE.activeUserId, + cipher = cipher, + attachment = attachment, + encryptedFilePath = "path/to/encrypted/file", + decryptedFilePath = "path/to/encrypted/file_decrypted", + ) + } returns Unit.asSuccess() + + assertEquals( + DownloadAttachmentResult.Success( + file = File("path/to/encrypted/file_decrypted"), + ), + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + ciphersService.getCipherAttachment( + cipherId = requireNotNull(cipherView.id), + attachmentId = attachmentId, + ) + fileManager.downloadFileToCache(url = "https://bitwarden.com") + vaultSdkSource.decryptFile( + userId = MOCK_USER_STATE.activeUserId, + cipher = cipher, + attachment = attachment, + encryptedFilePath = "path/to/encrypted/file", + decryptedFilePath = "path/to/encrypted/file_decrypted", + ) + fileManager.delete(file) + } + } + + private fun setupMockUri( + url: String, + queryParams: Map = emptyMap(), + ): Uri { + val mockUri = mockk { + queryParams.forEach { + every { getQueryParameter(it.key) } returns it.value + } + } + every { Uri.parse(url) } returns mockUri + return mockUri + } +} + +private val MOCK_PROFILE = AccountJson.Profile( + userId = "mockId-1", + email = "email", + isEmailVerified = true, + name = null, + stamp = "mockSecurityStamp-1", + organizationId = null, + avatarColorHex = null, + hasPremium = false, + forcePasswordResetReason = null, + kdfType = null, + kdfIterations = null, + kdfMemory = null, + kdfParallelism = null, + userDecryptionOptions = null, +) + +private val MOCK_ACCOUNT = AccountJson( + profile = MOCK_PROFILE, + tokens = AccountTokensJson( + accessToken = "accessToken", + refreshToken = "refreshToken", + ), + settings = AccountJson.Settings( + environmentUrlData = null, + ), +) + +private val MOCK_USER_STATE = UserStateJson( + activeUserId = "mockId-1", + accounts = mapOf( + "mockId-1" to MOCK_ACCOUNT, + ), +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 1ccf2daff3..ae771e1735 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.repository import android.net.Uri import app.cash.turbine.test import app.cash.turbine.turbineScope -import com.bitwarden.core.Attachment import com.bitwarden.core.Cipher import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView @@ -37,21 +36,14 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource -import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest -import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateSendJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SendTypeJson -import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson -import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest -import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachmentJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher -import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockDomains import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFileSendResponseJson @@ -69,38 +61,28 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAttachmentView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCollection import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView +import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager -import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem -import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult -import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult -import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult -import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData -import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult 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.RemovePasswordSendResult -import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult import com.x8bit.bitwarden.data.vault.repository.model.SendData -import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult -import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData @@ -185,6 +167,7 @@ class VaultRepositoryTest { emptyList(), ) private val mutableUnlockedUserIdsStateFlow = MutableStateFlow>(emptySet()) + private val cipherManager: CipherManager = mockk() private val vaultLockManager: VaultLockManager = mockk { every { vaultUnlockDataStateFlow } returns mutableVaultStateFlow every { @@ -234,6 +217,7 @@ class VaultRepositoryTest { dispatcherManager = dispatcherManager, totpCodeManager = totpCodeManager, pushManager = pushManager, + cipherManager = cipherManager, fileManager = fileManager, clock = clock, userLogoutManager = userLogoutManager, @@ -1908,637 +1892,6 @@ class VaultRepositoryTest { } } - @Test - fun `createCipher with no active user should return CreateCipherResult failure`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.createCipher(cipherView = mockk()) - - assertEquals( - CreateCipherResult.Error, - result, - ) - } - - @Test - fun `createCipher with encryptCipher failure should return CreateCipherResult failure`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns IllegalStateException().asFailure() - - val result = vaultRepository.createCipher(cipherView = mockCipherView) - - assertEquals( - CreateCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `createCipher with ciphersService createCipher failure should return CreateCipherResult failure`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.createCipher( - body = createMockCipherJsonRequest(number = 1, hasNullUri = true), - ) - } returns IllegalStateException().asFailure() - - val result = vaultRepository.createCipher(cipherView = mockCipherView) - - assertEquals( - CreateCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `createCipher with ciphersService createCipher success should return CreateCipherResult success`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - val mockCipher = createMockCipher(number = 1) - coEvery { - ciphersService.createCipher( - body = createMockCipherJsonRequest(number = 1, hasNullUri = true), - ) - } returns mockCipher.asSuccess() - coEvery { vaultDiskSource.saveCipher(userId, mockCipher) } just runs - - val result = vaultRepository.createCipher(cipherView = mockCipherView) - - assertEquals( - CreateCipherResult.Success, - result, - ) - } - - @Test - fun `createCipherInOrganization with no active user should return CreateCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.createCipherInOrganization( - cipherView = mockk(), - collectionIds = mockk(), - ) - - assertEquals( - CreateCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `createCipherInOrganization with encryptCipher failure should return CreateCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns IllegalStateException().asFailure() - - val result = vaultRepository.createCipherInOrganization( - cipherView = mockCipherView, - collectionIds = mockk(), - ) - - assertEquals( - CreateCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `createCipherInOrganization with ciphersService createCipher failure should return CreateCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.createCipherInOrganization( - body = CreateCipherInOrganizationJsonRequest( - cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), - collectionIds = listOf("mockId-1"), - ), - ) - } returns IllegalStateException().asFailure() - - val result = vaultRepository.createCipherInOrganization( - cipherView = mockCipherView, - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - CreateCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `createCipherInOrganization with ciphersService createCipher success should return CreateCipherResult success`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - val mockCipher = createMockCipher(number = 1) - coEvery { - ciphersService.createCipherInOrganization( - body = CreateCipherInOrganizationJsonRequest( - cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), - collectionIds = listOf("mockId-1"), - ), - ) - } returns mockCipher.asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId, - mockCipher.copy(collectionIds = listOf("mockId-1")), - ) - } just runs - - val result = vaultRepository.createCipherInOrganization( - cipherView = mockCipherView, - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - CreateCipherResult.Success, - result, - ) - } - - @Test - fun `updateCipher with no active user should return UpdateCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.updateCipher( - cipherId = "cipherId", - cipherView = mockk(), - ) - - assertEquals( - UpdateCipherResult.Error(null), - result, - ) - } - - @Test - fun `updateCipher with encryptCipher failure should return UpdateCipherResult failure`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId1234" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns IllegalStateException().asFailure() - - val result = vaultRepository.updateCipher( - cipherId = cipherId, - cipherView = mockCipherView, - ) - - assertEquals(UpdateCipherResult.Error(errorMessage = null), result) - } - - @Test - @Suppress("MaxLineLength") - fun `updateCipher with ciphersService updateCipher failure should return UpdateCipherResult Error with a null message`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId1234" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.updateCipher( - cipherId = cipherId, - body = createMockCipherJsonRequest(number = 1, hasNullUri = true), - ) - } returns IllegalStateException().asFailure() - - val result = vaultRepository.updateCipher( - cipherId = cipherId, - cipherView = mockCipherView, - ) - - assertEquals(UpdateCipherResult.Error(errorMessage = null), result) - } - - @Test - @Suppress("MaxLineLength") - fun `updateCipher with ciphersService updateCipher Invalid response should return UpdateCipherResult Error with a non-null message`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId1234" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.updateCipher( - cipherId = cipherId, - body = createMockCipherJsonRequest(number = 1, hasNullUri = true), - ) - } returns UpdateCipherResponseJson - .Invalid( - message = "You do not have permission to edit this.", - validationErrors = null, - ) - .asSuccess() - - val result = vaultRepository.updateCipher( - cipherId = cipherId, - cipherView = mockCipherView, - ) - - assertEquals( - UpdateCipherResult.Error( - errorMessage = "You do not have permission to edit this.", - ), - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `updateCipher with ciphersService updateCipher Success response should return UpdateCipherResult success`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId1234" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = mockCipherView, - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - val mockCipher = createMockCipher(number = 1) - coEvery { - ciphersService.updateCipher( - cipherId = cipherId, - body = createMockCipherJsonRequest(number = 1, hasNullUri = true), - ) - } returns UpdateCipherResponseJson - .Success(cipher = mockCipher) - .asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId = userId, - cipher = mockCipher.copy(collectionIds = mockCipherView.collectionIds), - ) - } just runs - - val result = vaultRepository.updateCipher( - cipherId = cipherId, - cipherView = mockCipherView, - ) - - assertEquals(UpdateCipherResult.Success, result) - } - - @Test - fun `hardDeleteCipher with no active user should return DeleteCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.hardDeleteCipher( - cipherId = "cipherId", - ) - - assertEquals( - DeleteCipherResult.Error, - result, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `hardDeleteCipher with ciphersService hardDeleteCipher failure should return DeleteCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val cipherId = "mockId-1" - coEvery { - ciphersService.hardDeleteCipher(cipherId = cipherId) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.hardDeleteCipher(cipherId) - - assertEquals(DeleteCipherResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `hardDeleteCipher with ciphersService hardDeleteCipher success should return DeleteCipherResult success`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "mockId-1" - coEvery { ciphersService.hardDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() - coEvery { vaultDiskSource.deleteCipher(userId, cipherId) } just runs - - val result = vaultRepository.hardDeleteCipher(cipherId) - - assertEquals(DeleteCipherResult.Success, result) - } - - @Test - fun `softDeleteCipher with no active user should return DeleteCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.softDeleteCipher( - cipherId = "cipherId", - cipherView = mockk(), - ) - - assertEquals( - DeleteCipherResult.Error, - result, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `softDeleteCipher with ciphersService softDeleteCipher failure should return DeleteCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val cipherId = "mockId-1" - coEvery { - ciphersService.softDeleteCipher(cipherId = cipherId) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.softDeleteCipher( - cipherId = cipherId, - cipherView = createMockCipherView(number = 1), - ) - - assertEquals(DeleteCipherResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `softDeleteCipher with ciphersService softDeleteCipher success should return DeleteCipherResult success`() = - runTest { - mockkStatic(Cipher::toEncryptedNetworkCipherResponse) - every { - createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() - } returns createMockCipher(number = 1) - val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") - val userId = "mockId-1" - val cipherId = "mockId-1" - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = createMockCipherView(number = 1) - .copy( - deletedDate = fixedInstant, - ), - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { ciphersService.softDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId = userId, - cipher = createMockCipher(number = 1), - ) - } just runs - val cipherView = createMockCipherView(number = 1) - mockkStatic(Instant::class) - every { Instant.now() } returns fixedInstant - - val result = vaultRepository.softDeleteCipher( - cipherId = cipherId, - cipherView = cipherView, - ) - - assertEquals(DeleteCipherResult.Success, result) - unmockkStatic(Instant::class) - unmockkStatic(Cipher::toEncryptedNetworkCipherResponse) - } - - @Test - fun `deleteCipherAttachment with no active user should return DeleteAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.deleteCipherAttachment( - cipherId = "cipherId", - attachmentId = "attachmentId", - cipherView = mockk(), - ) - - assertEquals( - DeleteAttachmentResult.Error, - result, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `deleteCipherAttachment with ciphersService deleteCipherAttachment failure should return DeleteAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val cipherId = "mockId-1" - val attachmentId = "mockId-1" - coEvery { - ciphersService.deleteCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - ) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.deleteCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - cipherView = createMockCipherView(number = 1), - ) - - assertEquals(DeleteAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `deleteCipherAttachment with ciphersService deleteCipherAttachment success should return DeleteAttachmentResult success`() = - runTest { - mockkStatic(Cipher::toEncryptedNetworkCipherResponse) - every { - createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() - } returns createMockCipher(number = 1) - val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") - val userId = "mockId-1" - val cipherId = "mockId-1" - val attachmentId = "mockId-1" - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = createMockCipherView(number = 1).copy( - attachments = emptyList(), - ), - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { - ciphersService.deleteCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - ) - } returns Unit.asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId = userId, - cipher = createMockCipher(number = 1), - ) - } just runs - val cipherView = createMockCipherView(number = 1) - mockkStatic(Instant::class) - every { Instant.now() } returns fixedInstant - - val result = vaultRepository.deleteCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - cipherView = cipherView, - ) - - assertEquals(DeleteAttachmentResult.Success, result) - unmockkStatic(Instant::class) - unmockkStatic(Cipher::toEncryptedNetworkCipherResponse) - } - - @Test - fun `restoreCipher with no active user should return RestoreCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.restoreCipher( - cipherId = "cipherId", - cipherView = mockk(), - ) - - assertEquals( - RestoreCipherResult.Error, - result, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `restoreCipher with ciphersService restoreCipher failure should return RestoreCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val cipherId = "mockId-1" - coEvery { - ciphersService.restoreCipher(cipherId = cipherId) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.restoreCipher( - cipherId = cipherId, - cipherView = createMockCipherView(number = 1), - ) - - assertEquals(RestoreCipherResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `restoreCipher with ciphersService restoreCipher success should return RestoreCipherResult success`() = - runTest { - mockkStatic(Cipher::toEncryptedNetworkCipherResponse) - every { - createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() - } returns createMockCipher(number = 1) - val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") - val userId = "mockId-1" - val cipherId = "mockId-1" - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = createMockCipherView(number = 1) - .copy( - deletedDate = null, - ), - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { ciphersService.restoreCipher(cipherId = cipherId) } returns Unit.asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId = userId, - cipher = createMockCipher(number = 1), - ) - } just runs - val cipherView = createMockCipherView(number = 1) - mockkStatic(Instant::class) - every { Instant.now() } returns fixedInstant - - val result = vaultRepository.restoreCipher( - cipherId = cipherId, - cipherView = cipherView, - ) - - assertEquals(RestoreCipherResult.Success, result) - } - @Test fun `createSend with no active user should return CreateSendResult Error`() = runTest { @@ -3087,1116 +2440,6 @@ class VaultRepositoryTest { assertEquals(DeleteSendResult.Success, result) } - @Test - fun `shareCipher with no active user should return ShareCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.shareCipher( - cipherId = "cipherId", - organizationId = "organizationId", - cipherView = mockk(), - collectionIds = emptyList(), - ) - - assertEquals( - ShareCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `shareCipher with cipherService shareCipher success should return ShareCipherResultSuccess`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val organizationId = "organizationId" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.moveToOrganization( - userId = userId, - organizationId = organizationId, - cipherView = createMockCipherView(number = 1), - ) - } returns mockCipherView.asSuccess() - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.shareCipher( - cipherId = "mockId-1", - body = ShareCipherJsonRequest( - cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), - collectionIds = listOf("mockId-1"), - ), - ) - } returns createMockCipher(number = 1).asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId, - createMockCipher(number = 1) - .copy(collectionIds = listOf("mockId-1")), - ) - } just runs - - val result = vaultRepository.shareCipher( - cipherId = "mockId-1", - organizationId = organizationId, - cipherView = createMockCipherView(number = 1), - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - ShareCipherResult.Success, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `shareCipher with cipherService shareCipher failure should return ShareCipherResultError`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val organizationId = "organizationId" - val mockCipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.moveToOrganization( - userId = userId, - organizationId = organizationId, - cipherView = createMockCipherView(number = 1), - ) - } returns mockCipherView.asSuccess() - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.shareCipher( - cipherId = "mockId-1", - body = ShareCipherJsonRequest( - cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), - collectionIds = listOf("mockId-1"), - ), - ) - } returns Throwable("Fail").asFailure() - coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs - - val result = vaultRepository.shareCipher( - cipherId = "mockId-1", - organizationId = organizationId, - cipherView = createMockCipherView(number = 1), - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - ShareCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `shareCipher with cipherService encryptCipher failure should return ShareCipherResultError`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val organizationId = "organizationId" - coEvery { - vaultSdkSource.moveToOrganization( - userId = userId, - organizationId = organizationId, - cipherView = createMockCipherView(number = 1), - ) - } returns Throwable("Fail").asFailure() - coEvery { - ciphersService.shareCipher( - cipherId = "mockId-1", - body = ShareCipherJsonRequest( - cipher = createMockCipherJsonRequest(number = 1), - collectionIds = listOf("mockId-1"), - ), - ) - } returns createMockCipher(number = 1).asSuccess() - coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs - - val result = vaultRepository.shareCipher( - cipherId = "mockId-1", - organizationId = organizationId, - cipherView = createMockCipherView(number = 1), - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - ShareCipherResult.Error, - result, - ) - } - - @Test - fun `updateCipherCollections with no active user should return ShareCipherResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.updateCipherCollections( - cipherId = "cipherId", - cipherView = mockk(), - collectionIds = emptyList(), - ) - - assertEquals( - ShareCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `updateCipherCollections with cipherService updateCipherCollections success should return ShareCipherResultSuccess`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = createMockCipherView(number = 1), - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.updateCipherCollections( - cipherId = "mockId-1", - body = UpdateCipherCollectionsJsonRequest( - collectionIds = listOf("mockId-1"), - ), - ) - } returns Unit.asSuccess() - coEvery { vaultDiskSource.saveCipher(userId, any()) } just runs - - val result = vaultRepository.updateCipherCollections( - cipherId = "mockId-1", - cipherView = createMockCipherView(number = 1) - .copy(collectionIds = listOf("mockId-1")), - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - ShareCipherResult.Success, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `updateCipherCollections with updateCipherCollections shareCipher failure should return ShareCipherResultError`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = createMockCipherView(number = 1), - ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() - coEvery { - ciphersService.updateCipherCollections( - cipherId = "mockId-1", - body = UpdateCipherCollectionsJsonRequest( - collectionIds = listOf("mockId-1"), - ), - ) - } returns Throwable("Fail").asFailure() - coEvery { vaultDiskSource.saveCipher(userId, any()) } just runs - - val result = vaultRepository.updateCipherCollections( - cipherId = "mockId-1", - cipherView = createMockCipherView(number = 1) - .copy(collectionIds = listOf("mockId-1")), - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - ShareCipherResult.Error, - result, - ) - } - - @Test - @Suppress("MaxLineLength") - fun `updateCipherCollections with updateCipherCollections encryptCipher failure should return ShareCipherResultError`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = createMockCipherView(number = 1), - ) - } returns Throwable("Fail").asFailure() - coEvery { - ciphersService.updateCipherCollections( - cipherId = "mockId-1", - body = UpdateCipherCollectionsJsonRequest( - collectionIds = listOf("mockId-1"), - ), - ) - } returns Unit.asSuccess() - coEvery { vaultDiskSource.saveCipher(userId, any()) } just runs - - val result = vaultRepository.updateCipherCollections( - cipherId = "mockId-1", - cipherView = createMockCipherView(number = 1) - .copy(collectionIds = listOf("mockId-1")), - collectionIds = listOf("mockId-1"), - ) - - assertEquals( - ShareCipherResult.Error, - result, - ) - } - - @Test - fun `createAttachment with no active user should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = null - - val result = vaultRepository.createAttachment( - cipherId = "cipherId", - cipherView = mockk(), - fileSizeBytes = "mockFileSize", - fileName = "mockFileName", - fileUri = mockk(), - ) - - assertEquals( - CreateAttachmentResult.Error, - result, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with encryptCipher failure should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with encryptAttachment failure should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFile = File.createTempFile("mockFile", "temp") - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with uriToByteArray failure should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with createAttachment failure should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - val mockFile = File.createTempFile("mockFile", "temp") - val mockAttachment = createMockSdkAttachment(number = 1) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns mockAttachment.asSuccess() - coEvery { - ciphersService.createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - fileName = mockFileName, - key = "mockKey-1", - fileSize = mockFileSize, - ), - ) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with uploadAttachment failure should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - val mockFile = File.createTempFile("mockFile", "temp") - val mockAttachment = createMockSdkAttachment(number = 1) - val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns mockAttachment.asSuccess() - coEvery { - ciphersService.createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - fileName = mockFileName, - key = "mockKey-1", - fileSize = mockFileSize, - ), - ) - } returns mockAttachmentJsonResponse.asSuccess() - coEvery { - ciphersService.uploadAttachment( - attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = File("${mockFile.absoluteFile}.enc"), - ) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with decryptCipher failure should return CreateAttachmentResult Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - val mockFile = File.createTempFile("mockFile", "temp") - val mockAttachment = createMockSdkAttachment(number = 1) - val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) - val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) - val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( - collectionIds = listOf("mockId-1"), - ) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns mockAttachment.asSuccess() - coEvery { - ciphersService.createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - fileName = mockFileName, - key = "mockKey-1", - fileSize = mockFileSize, - ), - ) - } returns mockAttachmentJsonResponse.asSuccess() - coEvery { - ciphersService.uploadAttachment( - attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = File("${mockFile.absoluteFile}.enc"), - ) - } returns mockCipherResponse.asSuccess() - coEvery { - vaultDiskSource.saveCipher(userId = userId, cipher = mockUpdatedCipherResponse) - } just runs - coEvery { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = mockUpdatedCipherResponse.toEncryptedSdkCipher(), - ) - } returns Throwable("Fail").asFailure() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Error, result) - } - - @Suppress("MaxLineLength") - @Test - fun `createAttachment with createAttachment success should return CreateAttachmentResult Success`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - val mockFile = File.createTempFile("mockFile", "temp") - val mockAttachment = createMockSdkAttachment(number = 1) - val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) - val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) - val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( - collectionIds = listOf("mockId-1"), - ) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns mockAttachment.asSuccess() - coEvery { - ciphersService.createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - fileName = mockFileName, - key = "mockKey-1", - fileSize = mockFileSize, - ), - ) - } returns mockAttachmentJsonResponse.asSuccess() - coEvery { - ciphersService.uploadAttachment( - attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = File("${mockFile.absolutePath}.enc"), - ) - } returns mockCipherResponse.asSuccess() - coEvery { - vaultDiskSource.saveCipher(userId = userId, cipher = mockUpdatedCipherResponse) - } just runs - coEvery { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = mockUpdatedCipherResponse.toEncryptedSdkCipher(), - ) - } returns mockCipherView.asSuccess() - - val result = vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - assertEquals(CreateAttachmentResult.Success(mockCipherView), result) - } - - @Test - fun `createAttachment should delete temp files after upload success`() { - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - val mockFile = File.createTempFile("mockFile", "temp") - val mockAttachment = createMockSdkAttachment(number = 1) - val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) - val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) - val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( - collectionIds = listOf("mockId-1"), - ) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns mockAttachment.asSuccess() - coEvery { - ciphersService.createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - fileName = mockFileName, - key = "mockKey-1", - fileSize = mockFileSize, - ), - ) - } returns mockAttachmentJsonResponse.asSuccess() - coEvery { - ciphersService.uploadAttachment( - attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = File("${mockFile.absolutePath}.enc"), - ) - } returns mockCipherResponse.asSuccess() - coEvery { - vaultDiskSource.saveCipher(userId = userId, cipher = mockUpdatedCipherResponse) - } just runs - coEvery { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = mockUpdatedCipherResponse.toEncryptedSdkCipher(), - ) - } returns mockCipherView.asSuccess() - - vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - coVerify { - fileManager.delete(*anyVararg()) - } - } - } - - @Test - fun `createAttachment should delete temp files after upload failure`() { - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val cipherId = "cipherId-1" - val mockUri = setupMockUri(url = "www.test.com") - val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1, clock = clock) - val mockFileName = "mockFileName-1" - val mockFileSize = "1" - val mockAttachmentView = createMockAttachmentView(number = 1).copy( - sizeName = null, - id = null, - url = null, - key = null, - ) - val mockFile = File.createTempFile("mockFile", "temp") - val mockAttachment = createMockSdkAttachment(number = 1) - val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() - coEvery { - fileManager.writeUriToCache(fileUri = mockUri) - } returns mockFile.asSuccess() - coEvery { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = mockCipher, - attachmentView = mockAttachmentView, - decryptedFilePath = mockFile.absolutePath, - encryptedFilePath = "${mockFile.absolutePath}.enc", - ) - } returns mockAttachment.asSuccess() - coEvery { - ciphersService.createAttachment( - cipherId = cipherId, - body = AttachmentJsonRequest( - fileName = mockFileName, - key = "mockKey-1", - fileSize = mockFileSize, - ), - ) - } returns mockAttachmentJsonResponse.asSuccess() - coEvery { - ciphersService.uploadAttachment( - attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = File("${mockFile.absolutePath}.enc"), - ) - } returns Throwable("Fail").asFailure() - - vaultRepository.createAttachment( - cipherId = cipherId, - cipherView = mockCipherView, - fileSizeBytes = mockFileSize, - fileName = mockFileName, - fileUri = mockUri, - ) - - coVerify { - fileManager.delete(*anyVararg()) - } - } - } - - @Test - fun `downloadAttachment with missing attachment should return Failure`() = runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - - val attachmentId = "mockId-1" - val cipher = mockk { - every { attachments } returns emptyList() - every { id } returns "mockId-1" - } - val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } returns cipher.asSuccess() - - assertEquals( - DownloadAttachmentResult.Failure, - vaultRepository.downloadAttachment( - cipherView = cipherView, - attachmentId = attachmentId, - ), - ) - - coVerify(exactly = 1) { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } - coVerify(exactly = 0) { - ciphersService.getCipherAttachment(any(), any()) - } - } - - @Test - fun `downloadAttachment with failed attachment details request should return Failure`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - - val attachmentId = "mockId-1" - val attachment = mockk { - every { id } returns attachmentId - } - val cipher = mockk { - every { attachments } returns listOf(attachment) - every { id } returns "mockId-1" - } - val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } returns cipher.asSuccess() - - coEvery { - ciphersService.getCipherAttachment(any(), any()) - } returns Throwable().asFailure() - - assertEquals( - DownloadAttachmentResult.Failure, - vaultRepository.downloadAttachment( - cipherView = cipherView, - attachmentId = attachmentId, - ), - ) - - coVerify(exactly = 1) { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - ciphersService.getCipherAttachment( - cipherId = requireNotNull(cipherView.id), - attachmentId = attachmentId, - ) - } - } - - @Test - fun `downloadAttachment with attachment details missing url should return Failure`() = runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - - val attachmentId = "mockId-1" - val attachment = mockk { - every { id } returns attachmentId - } - val cipher = mockk { - every { attachments } returns listOf(attachment) - every { id } returns "mockId-1" - } - val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } returns cipher.asSuccess() - - val response = mockk { - every { url } returns null - } - coEvery { - ciphersService.getCipherAttachment(any(), any()) - } returns response.asSuccess() - - assertEquals( - DownloadAttachmentResult.Failure, - vaultRepository.downloadAttachment( - cipherView = cipherView, - attachmentId = attachmentId, - ), - ) - - coVerify(exactly = 1) { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - ciphersService.getCipherAttachment( - cipherId = requireNotNull(cipherView.id), - attachmentId = attachmentId, - ) - } - } - - @Test - fun `downloadAttachment with failed download should return Failure`() = runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - - val attachmentId = "mockId-1" - val attachment = mockk { - every { id } returns attachmentId - } - val cipher = mockk { - every { attachments } returns listOf(attachment) - every { id } returns "mockId-1" - } - - val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } returns cipher.asSuccess() - - val response = mockk { - every { url } returns "https://bitwarden.com" - } - coEvery { - ciphersService.getCipherAttachment(any(), any()) - } returns response.asSuccess() - - coEvery { - fileManager.downloadFileToCache(any()) - } returns DownloadResult.Failure - - assertEquals( - DownloadAttachmentResult.Failure, - vaultRepository.downloadAttachment( - cipherView = cipherView, - attachmentId = attachmentId, - ), - ) - - coVerify(exactly = 1) { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - ciphersService.getCipherAttachment( - cipherId = requireNotNull(cipherView.id), - attachmentId = attachmentId, - ) - fileManager.downloadFileToCache("https://bitwarden.com") - } - } - - @Test - fun `downloadAttachment with failed decryption should delete file and return Failure`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - - val attachmentId = "mockId-1" - val attachment = mockk { - every { id } returns attachmentId - } - val cipher = mockk { - every { attachments } returns listOf(attachment) - every { id } returns "mockId-1" - } - val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } returns cipher.asSuccess() - - val response = mockk { - every { url } returns "https://bitwarden.com" - } - coEvery { - ciphersService.getCipherAttachment(any(), any()) - } returns response.asSuccess() - - val file = mockk { - every { path } returns "path/to/encrypted/file" - every { delete() } returns true - } - coEvery { - fileManager.downloadFileToCache(any()) - } returns DownloadResult.Success(file) - - coEvery { - vaultSdkSource.decryptFile( - userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, - encryptedFilePath = "path/to/encrypted/file", - decryptedFilePath = "path/to/encrypted/file_decrypted", - ) - } returns Throwable().asFailure() - - assertEquals( - DownloadAttachmentResult.Failure, - vaultRepository.downloadAttachment( - cipherView = cipherView, - attachmentId = attachmentId, - ), - ) - - coVerify(exactly = 1) { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - ciphersService.getCipherAttachment( - cipherId = requireNotNull(cipherView.id), - attachmentId = attachmentId, - ) - fileManager.downloadFileToCache("https://bitwarden.com") - vaultSdkSource.decryptFile( - userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, - encryptedFilePath = "path/to/encrypted/file", - decryptedFilePath = "path/to/encrypted/file_decrypted", - ) - } - verify(exactly = 1) { - file.delete() - } - } - - @Test - fun `downloadAttachment with successful decryption should delete file and return Success`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - - val attachmentId = "mockId-1" - val attachment = mockk { - every { id } returns attachmentId - } - val cipher = mockk { - every { attachments } returns listOf(attachment) - every { id } returns "mockId-1" - } - val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - } returns cipher.asSuccess() - - val response = mockk { - every { url } returns "https://bitwarden.com" - } - coEvery { - ciphersService.getCipherAttachment(any(), any()) - } returns response.asSuccess() - - val file = mockk { - every { path } returns "path/to/encrypted/file" - every { delete() } returns true - } - coEvery { - fileManager.downloadFileToCache(any()) - } returns DownloadResult.Success(file) - - coEvery { - vaultSdkSource.decryptFile( - userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, - encryptedFilePath = "path/to/encrypted/file", - decryptedFilePath = "path/to/encrypted/file_decrypted", - ) - } returns Unit.asSuccess() - - assertEquals( - DownloadAttachmentResult.Success( - file = File("path/to/encrypted/file_decrypted"), - ), - vaultRepository.downloadAttachment( - cipherView = cipherView, - attachmentId = attachmentId, - ), - ) - - coVerify(exactly = 1) { - vaultSdkSource.encryptCipher(MOCK_USER_STATE.activeUserId, cipherView) - ciphersService.getCipherAttachment( - cipherId = requireNotNull(cipherView.id), - attachmentId = attachmentId, - ) - fileManager.downloadFileToCache("https://bitwarden.com") - vaultSdkSource.decryptFile( - userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, - encryptedFilePath = "path/to/encrypted/file", - decryptedFilePath = "path/to/encrypted/file_decrypted", - ) - } - verify(exactly = 1) { - file.delete() - } - } - @Test fun `generateTotp with no active user should return GenerateTotpResult Error`() = runTest {