From f04e9906f61196b1d194e095066e9f8990691083 Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Sun, 28 Jan 2024 10:50:14 -0600 Subject: [PATCH] Adding the Repository folder calls. (#813) --- .../vault/datasource/disk/VaultDiskSource.kt | 5 + .../datasource/disk/VaultDiskSourceImpl.kt | 4 + .../vault/datasource/sdk/VaultSdkSource.kt | 12 + .../datasource/sdk/VaultSdkSourceImpl.kt | 11 + .../data/vault/repository/VaultRepository.kt | 18 + .../vault/repository/VaultRepositoryImpl.kt | 98 +++++ .../repository/di/VaultRepositoryModule.kt | 3 + .../repository/model/CreateFolderResult.kt | 19 + .../repository/model/DeleteFolderResult.kt | 17 + .../repository/model/UpdateFolderResult.kt | 20 + .../util/VaultSdkFolderExtensions.kt | 8 + .../datasource/disk/VaultDiskSourceTest.kt | 12 + .../datasource/sdk/VaultSdkSourceTest.kt | 28 ++ .../vault/repository/VaultRepositoryTest.kt | 390 +++++++++++++++++- .../util/VaultSdkFolderExtensionsTest.kt | 11 + 15 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt index 7eb2a0d40f..e39ad6c022 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt @@ -39,6 +39,11 @@ interface VaultDiskSource { */ fun getDomains(userId: String): Flow + /** + * Deletes a folder from the data source for the given [userId] and [folderId]. + */ + suspend fun deleteFolder(userId: String, folderId: String) + /** * Saves a folder to the data source for the given [userId]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt index 17e661549c..d11d972149 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt @@ -114,6 +114,10 @@ class VaultDiskSourceImpl( json.decodeFromString(entity.domainsJson) } + override suspend fun deleteFolder(userId: String, folderId: String) { + foldersDao.deleteFolder(userId = userId, folderId = folderId) + } + override suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder) { foldersDao.insertFolder( folder = FolderEntity( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt index d03927af71..5908980797 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt @@ -229,6 +229,18 @@ interface VaultSdkSource { sendList: List, ): Result> + /** + * Encrypts a [FolderView] for the user with the given [userId], returning a [Folder] wrapped + * in a [Result]. + * + * This should only be called after a successful call to [initializeCrypto] for the associated + * user. + */ + suspend fun encryptFolder( + userId: String, + folder: FolderView, + ): Result + /** * Decrypts a [Folder] for the user with the given [userId], returning a [FolderView] wrapped * in a [Result]. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt index 2cbeca2fa1..ae5efc6061 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt @@ -240,6 +240,17 @@ class VaultSdkSourceImpl( } } + override suspend fun encryptFolder( + userId: String, + folder: FolderView, + ): Result = + runCatching { + getClient(userId = userId) + .vault() + .folders() + .encrypt(folder) + } + override suspend fun decryptFolder( userId: String, folder: Folder, 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 210a5474cb..009b1d4256 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 @@ -13,9 +13,11 @@ 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.GenerateTotpResult @@ -25,6 +27,7 @@ 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 @@ -283,4 +286,19 @@ interface VaultRepository : VaultLockManager { attachmentId: String, cipherView: CipherView, ): DeleteAttachmentResult + + /** + * Attempt to create a folder. + */ + suspend fun createFolder(folderView: FolderView): CreateFolderResult + + /** + * Attempt to delete a folder. + */ + suspend fun deleteFolder(folderId: String): DeleteFolderResult + + /** + * Attempt to update a folder. + */ + suspend fun updateFolder(folderId: String, folderView: FolderView): UpdateFolderResult } 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 6baadbacf1..c13eb7dd95 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 @@ -31,8 +31,10 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonReq 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.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 +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 @@ -42,9 +44,11 @@ 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.GenerateTotpResult @@ -54,6 +58,7 @@ 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 @@ -61,10 +66,12 @@ 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 import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList @@ -80,6 +87,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn @@ -107,6 +115,7 @@ class VaultRepositoryImpl( private val syncService: SyncService, private val ciphersService: CiphersService, private val sendsService: SendsService, + private val folderService: FolderService, private val vaultDiskSource: VaultDiskSource, private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, @@ -917,6 +926,95 @@ class VaultRepositoryImpl( ) } + override suspend fun createFolder(folderView: FolderView): CreateFolderResult { + val userId = activeUserId ?: return CreateFolderResult.Error + return vaultSdkSource + .encryptFolder( + userId = userId, + folder = folderView, + ) + .flatMap { folder -> + folderService + .createFolder( + body = folder.toEncryptedNetworkFolder(), + ) + } + .onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) } + .flatMap { vaultSdkSource.decryptFolder(userId, it.toEncryptedSdkFolder()) } + .fold( + onSuccess = { CreateFolderResult.Success(folderView = it) }, + onFailure = { CreateFolderResult.Error }, + ) + } + + override suspend fun updateFolder( + folderId: String, + folderView: FolderView, + ): UpdateFolderResult { + val userId = activeUserId ?: return UpdateFolderResult.Error(null) + return vaultSdkSource + .encryptFolder( + userId = userId, + folder = folderView, + ) + .flatMap { folder -> + folderService + .updateFolder( + folderId = folder.id.toString(), + body = folder.toEncryptedNetworkFolder(), + ) + } + .fold( + onSuccess = { response -> + when (response) { + is UpdateFolderResponseJson.Success -> { + vaultDiskSource.saveFolder(userId, response.folder) + vaultSdkSource + .decryptFolder( + userId, + response.folder.toEncryptedSdkFolder(), + ) + .fold( + onSuccess = { UpdateFolderResult.Success(it) }, + onFailure = { UpdateFolderResult.Error(errorMessage = null) }, + ) + } + + is UpdateFolderResponseJson.Invalid -> { + UpdateFolderResult.Error(response.message) + } + } + }, + onFailure = { UpdateFolderResult.Error(it.message) }, + ) + } + + override suspend fun deleteFolder(folderId: String): DeleteFolderResult { + val userId = activeUserId ?: return DeleteFolderResult.Error + return folderService + .deleteFolder( + folderId = folderId, + ) + .onSuccess { + clearFolderIdFromCiphers(folderId, userId) + vaultDiskSource.deleteFolder(userId, folderId) + } + .fold( + onSuccess = { DeleteFolderResult.Success }, + onFailure = { DeleteFolderResult.Error }, + ) + } + + private suspend fun clearFolderIdFromCiphers(folderId: String, userId: String) { + vaultDiskSource.getCiphers(userId).firstOrNull()?.forEach { + if (it.folderId == folderId) { + vaultDiskSource.saveCipher( + userId, it.copy(folderId = null), + ) + } + } + } + /** * Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user * key. This indicates a scenario in which a user has requested PIN unlocking but requires 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 d034da1030..c7de1e3740 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 @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager 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.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 @@ -33,6 +34,7 @@ object VaultRepositoryModule { syncService: SyncService, sendsService: SendsService, ciphersService: CiphersService, + folderService: FolderService, vaultDiskSource: VaultDiskSource, vaultSdkSource: VaultSdkSource, authDiskSource: AuthDiskSource, @@ -46,6 +48,7 @@ object VaultRepositoryModule { syncService = syncService, sendsService = sendsService, ciphersService = ciphersService, + folderService = folderService, vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, authDiskSource = authDiskSource, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt new file mode 100644 index 0000000000..2d9969974a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +import com.bitwarden.core.FolderView + +/** + * Models result of creating a folder. + */ +sealed class CreateFolderResult { + + /** + * Folder created successfully. + */ + data class Success(val folderView: FolderView) : CreateFolderResult() + + /** + * Generic error while creating a folder. + */ + data object Error : CreateFolderResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt new file mode 100644 index 0000000000..27f90c632b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +/** + * Models result of deleting a folder. + */ +sealed class DeleteFolderResult { + + /** + * Folder deleted successfully. + */ + data object Success : DeleteFolderResult() + + /** + * Generic error while deleting a folder. + */ + data object Error : DeleteFolderResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt new file mode 100644 index 0000000000..68a4d90c68 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +import com.bitwarden.core.FolderView + +/** + * Models result of updating a folder. + */ +sealed class UpdateFolderResult { + + /** + * Folder updated successfully. + */ + data class Success(val folderView: FolderView) : UpdateFolderResult() + + /** + * Generic error while updating a folder. The optional [errorMessage] + * may be displayed directly in the UI when present. + */ + data class Error(val errorMessage: String?) : UpdateFolderResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensions.kt index 00de36f123..664effd8b4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensions.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository.util import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import java.util.Locale @@ -29,3 +30,10 @@ fun SyncResponseJson.Folder.toEncryptedSdkFolder(): Folder = @JvmName("toAlphabeticallySortedFolderList") fun List.sortAlphabetically(): List = this.sortedBy { it.name.uppercase(Locale.getDefault()) } + +/** + * Converts a Bitwarden SDK [Folder] objects to a corresponding + * [SyncResponseJson.Folder] object. + */ +fun Folder.toEncryptedNetworkFolder(): FolderJsonRequest = + FolderJsonRequest(name = name) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index 8c484171a4..6d21b54e05 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -138,6 +138,18 @@ class VaultDiskSourceTest { } } + @Test + fun `DeleteFolder should call deleteFolder`() = runTest { + assertFalse(foldersDao.deleteFolderCalled) + vaultDiskSource.saveFolder(USER_ID, FOLDER_1) + assertEquals(1, foldersDao.storedFolders.size) + + vaultDiskSource.deleteFolder(USER_ID, FOLDER_1.id) + + assertTrue(foldersDao.deleteFolderCalled) + assertEquals(emptyList(), foldersDao.storedFolders) + } + @Test fun `saveFolder should call insertFolder`() = runTest { assertFalse(foldersDao.insertFolderCalled) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt index aed7bf46d4..fbc47c16c3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt @@ -581,6 +581,34 @@ class VaultSdkSourceTest { verify { sdkClientManager.getOrCreateClient(userId = userId) } } + @Test + fun `encryptFolder should call SDK and return a Result with correct data`() = runBlocking { + val userId = "userId" + val expectedResult = mockk() + val mockFolder = mockk() + coEvery { + clientVault.folders().encrypt( + folder = mockFolder, + ) + } returns expectedResult + + val result = vaultSdkSource.encryptFolder( + userId = userId, + folder = mockFolder, + ) + assertEquals( + expectedResult.asSuccess(), + result, + ) + + coVerify { + clientVault.folders().encrypt( + folder = mockFolder, + ) + } + verify { sdkClientManager.getOrCreateClient(userId = userId) } + } + @Test fun `Folder decrypt should call SDK and return a Result with correct data`() = runBlocking { val userId = "userId" 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 3361959fd5..14ab227d52 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 @@ -6,6 +6,7 @@ import com.bitwarden.core.Cipher import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView import com.bitwarden.core.DateTime +import com.bitwarden.core.Folder import com.bitwarden.core.FolderView import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod @@ -29,11 +30,13 @@ 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.FileUploadType +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson 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.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.createMockAttachmentEncryptResult import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachmentJsonResponse @@ -48,6 +51,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService +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 @@ -67,9 +71,11 @@ 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.GenerateTotpResult @@ -78,6 +84,7 @@ 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 import com.x8bit.bitwarden.data.vault.repository.model.VaultState @@ -88,6 +95,7 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherRe import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem @@ -113,6 +121,7 @@ import java.net.UnknownHostException import java.time.Clock import java.time.Instant import java.time.ZoneOffset +import java.time.ZonedDateTime import java.time.temporal.ChronoUnit @Suppress("LargeClass") @@ -128,6 +137,7 @@ class VaultRepositoryTest { private val syncService: SyncService = mockk() private val sendsService: SendsService = mockk() private val ciphersService: CiphersService = mockk() + private val folderService: FolderService = mockk() private val vaultDiskSource: VaultDiskSource = mockk() private val totpCodeManager: TotpCodeManager = mockk() private val vaultSdkSource: VaultSdkSource = mockk { @@ -151,6 +161,7 @@ class VaultRepositoryTest { syncService = syncService, sendsService = sendsService, ciphersService = ciphersService, + folderService = folderService, vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, authDiskSource = fakeAuthDiskSource, @@ -3137,6 +3148,375 @@ class VaultRepositoryTest { ) } + @Test + fun `deleteFolder with no active user should return DeleteFolderResult failure`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = vaultRepository.deleteFolder("Test") + + assertEquals( + DeleteFolderResult.Error, + result, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `DeleteFolder with folderService Delete failure should return DeleteFolderResult Failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderId = "mockId-1" + coEvery { folderService.deleteFolder(folderId) } returns Throwable("fail").asFailure() + + val result = vaultRepository.deleteFolder(folderId) + assertEquals(DeleteFolderResult.Error, result) + } + + @Suppress("MaxLineLength") + @Test + fun `DeleteFolder with folderService Delete success should return DeleteFolderResult Success and update ciphers`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderId = "mockFolderId-1" + coEvery { folderService.deleteFolder(folderId) } returns Unit.asSuccess() + coEvery { + vaultDiskSource.deleteFolder( + MOCK_USER_STATE.activeUserId, + folderId, + ) + } just runs + + val mockCipher = createMockCipher(1) + + val mutableCiphersStateFlow = + MutableStateFlow( + listOf( + mockCipher, + createMockCipher(2), + ), + ) + + coEvery { + vaultDiskSource.getCiphers(MOCK_USER_STATE.activeUserId) + } returns mutableCiphersStateFlow + + coEvery { + vaultDiskSource.saveCipher( + MOCK_USER_STATE.activeUserId, + mockCipher.copy( + folderId = null, + ), + ) + } just runs + + val result = vaultRepository.deleteFolder(folderId) + + coVerify(exactly = 1) { + vaultDiskSource.saveCipher( + MOCK_USER_STATE.activeUserId, + mockCipher.copy( + folderId = null, + ), + ) + } + + assertEquals(DeleteFolderResult.Success, result) + } + + @Test + fun `createFolder with no active user should return CreateFolderResult failure`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = vaultRepository.createFolder(mockk()) + + assertEquals( + CreateFolderResult.Error, + result, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `createFolder with folderService Delete failure should return DeleteFolderResult Failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderId = "mockId-1" + coEvery { folderService.deleteFolder(folderId) } returns Throwable("fail").asFailure() + + val result = vaultRepository.deleteFolder(folderId) + assertEquals(DeleteFolderResult.Error, result) + } + + @Test + fun `createFolder with encryptFolder failure should return CreateFolderResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderView = FolderView( + id = null, + name = "TestName", + revisionDate = DateTime.now(), + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.createFolder(folderView) + assertEquals(CreateFolderResult.Error, result) + } + + @Test + fun `createFolder with folderService failure should return CreateFolderResult failure`() = + runTest { + val date = DateTime.now() + val testFolderName = "TestName" + + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderView = FolderView( + id = null, + name = testFolderName, + revisionDate = date, + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns Folder(id = null, name = testFolderName, revisionDate = date).asSuccess() + + coEvery { + folderService.createFolder( + body = FolderJsonRequest(testFolderName), + ) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.createFolder(folderView) + assertEquals(CreateFolderResult.Error, result) + } + + @Test + fun `createFolder with folderService createFolder should return CreateFolderResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val date = DateTime.now() + val testFolderName = "TestName" + + val folderView = FolderView( + id = null, + name = testFolderName, + revisionDate = date, + ) + + val networkFolder = SyncResponseJson.Folder( + id = "1", + name = testFolderName, + revisionDate = ZonedDateTime.now(), + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns Folder(id = null, name = testFolderName, revisionDate = date).asSuccess() + + coEvery { + folderService.createFolder( + body = FolderJsonRequest(testFolderName), + ) + } returns networkFolder.asSuccess() + + coEvery { + vaultDiskSource.saveFolder( + MOCK_USER_STATE.activeUserId, + networkFolder, + ) + } just runs + + coEvery { + vaultSdkSource.decryptFolder( + MOCK_USER_STATE.activeUserId, + networkFolder.toEncryptedSdkFolder(), + ) + } returns folderView.asSuccess() + + val result = vaultRepository.createFolder(folderView) + assertEquals(CreateFolderResult.Success(folderView), result) + } + + @Test + fun `updateFolder with no active user should return UpdateFolderResult failure`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = vaultRepository.updateFolder("Test", mockk()) + + assertEquals( + UpdateFolderResult.Error(null), + result, + ) + } + + @Test + fun `updateFolder with encryptFolder failure should return UpdateFolderResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderId = "testId" + val folderView = FolderView( + id = folderId, + name = "TestName", + revisionDate = DateTime.now(), + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.updateFolder(folderId, folderView) + + assertEquals(UpdateFolderResult.Error(errorMessage = null), result) + } + + @Test + fun `updateFolder with folderService failure should return UpdateFolderResult failure`() = + runTest { + val date = DateTime.now() + val testFolderName = "TestName" + val folderId = "testId" + + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderView = FolderView( + id = folderId, + name = testFolderName, + revisionDate = date, + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess() + + coEvery { + folderService.updateFolder( + folderId = folderId, + body = FolderJsonRequest(testFolderName), + ) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.updateFolder(folderId, folderView) + assertEquals(UpdateFolderResult.Error(errorMessage = null), result) + } + + @Suppress("MaxLineLength") + @Test + fun `updateFolder with folderService updateFolder Invalid response should return UpdateFolderResult Error with a non-null message`() = + runTest { + val date = DateTime.now() + val testFolderName = "TestName" + val folderId = "testId" + + fakeAuthDiskSource.userState = MOCK_USER_STATE + val folderView = FolderView( + id = folderId, + name = testFolderName, + revisionDate = date, + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess() + + coEvery { + folderService.updateFolder( + folderId = folderId, + body = FolderJsonRequest(testFolderName), + ) + } returns UpdateFolderResponseJson + .Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ) + .asSuccess() + + val result = vaultRepository.updateFolder(folderId, folderView) + assertEquals( + UpdateFolderResult.Error( + errorMessage = "You do not have permission to edit this.", + ), + result, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `updateFolder with folderService updateFolder success should return UpdateFolderResult success`() = + runTest { + val date = DateTime.now() + val testFolderName = "TestName" + val folderId = "testId" + + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val folderView = FolderView( + id = folderId, + name = testFolderName, + revisionDate = date, + ) + + val networkFolder = SyncResponseJson.Folder( + id = "1", + name = testFolderName, + revisionDate = ZonedDateTime.now(), + ) + + coEvery { + vaultSdkSource.encryptFolder( + userId = MOCK_USER_STATE.activeUserId, + folder = folderView, + ) + } returns Folder(id = folderId, name = testFolderName, revisionDate = date).asSuccess() + + coEvery { + folderService.updateFolder( + folderId = folderId, + body = FolderJsonRequest(testFolderName), + ) + } returns UpdateFolderResponseJson + .Success(folder = networkFolder) + .asSuccess() + + coEvery { + vaultDiskSource.saveFolder( + MOCK_USER_STATE.activeUserId, + networkFolder, + ) + } just runs + + coEvery { + vaultSdkSource.decryptFolder( + MOCK_USER_STATE.activeUserId, + networkFolder.toEncryptedSdkFolder(), + ) + } returns folderView.asSuccess() + + val result = vaultRepository.updateFolder(folderId, folderView) + assertEquals(UpdateFolderResult.Success(folderView), result) + } + @Suppress("MaxLineLength") @Test fun `getAuthCodeFlow with no active user should emit an error`() = runTest { @@ -3169,7 +3549,10 @@ class VaultRepositoryTest { } just runs every { - settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant()) + settingsDiskSource.storeLastSyncTime( + MOCK_USER_STATE.activeUserId, + clock.instant(), + ) } just runs val stateFlow = MutableStateFlow>( @@ -3233,7 +3616,10 @@ class VaultRepositoryTest { ) } just runs every { - settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant()) + settingsDiskSource.storeLastSyncTime( + MOCK_USER_STATE.activeUserId, + clock.instant(), + ) } just runs val stateFlow = MutableStateFlow>>( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensionsTest.kt index a21cef9701..2b9646800e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkFolderExtensionsTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.repository.util +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder @@ -58,4 +59,14 @@ class VaultSdkFolderExtensionsTest { list.sortAlphabetically(), ) } + + @Test + fun `toEncryptedNetworkFolder should convert a SdkFolder to a NetworkFolder`() { + val sdkFolder = createMockSdkFolder(number = 1) + val syncFolder = sdkFolder.toEncryptedNetworkFolder() + assertEquals( + FolderJsonRequest(sdkFolder.name), + syncFolder, + ) + } }