diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt index d2908d1a6..1f4124c1a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRe 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 java.io.File /** * Provides an API for querying ciphers endpoints. @@ -31,7 +32,7 @@ interface CiphersService { */ suspend fun uploadAttachment( attachmentJsonResponse: AttachmentJsonResponse, - encryptedFile: ByteArray, + encryptedFile: File, ): Result /** diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt index fe5f8036c..e79769318 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt @@ -17,7 +17,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherRespo import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File import java.time.Clock import java.time.ZoneOffset import java.time.ZonedDateTime @@ -48,7 +49,7 @@ class CiphersServiceImpl( override suspend fun uploadAttachment( attachmentJsonResponse: AttachmentJsonResponse, - encryptedFile: ByteArray, + encryptedFile: File, ): Result { val cipher = attachmentJsonResponse.cipherResponse return when (attachmentJsonResponse.fileUploadType) { @@ -62,7 +63,7 @@ class CiphersServiceImpl( ) .addPart( part = MultipartBody.Part.createFormData( - body = encryptedFile.toRequestBody( + body = encryptedFile.asRequestBody( contentType = "application/octet-stream".toMediaType(), ), name = "data", @@ -83,7 +84,7 @@ class CiphersServiceImpl( .RFC_1123_DATE_TIME .format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)), version = attachmentJsonResponse.url.toUri().getQueryParameter("sv"), - body = encryptedFile.toRequestBody(), + body = encryptedFile.asRequestBody(), ) } } 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 518994bf2..434b2af76 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 @@ -137,8 +137,9 @@ interface VaultSdkSource { userId: String, cipher: Cipher, attachmentView: AttachmentView, - fileBuffer: ByteArray, - ): Result + decryptedFilePath: String, + encryptedFilePath: String, + ): Result /** * Encrypts a [CipherView] for the user with the given [userId], returning a [Cipher] wrapped 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 e080210b6..d094f6f1a 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 @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk import com.bitwarden.core.Attachment -import com.bitwarden.core.AttachmentEncryptResult import com.bitwarden.core.AttachmentView import com.bitwarden.core.Cipher import com.bitwarden.core.CipherListView @@ -183,16 +182,18 @@ class VaultSdkSourceImpl( userId: String, cipher: Cipher, attachmentView: AttachmentView, - fileBuffer: ByteArray, - ): Result = + decryptedFilePath: String, + encryptedFilePath: String, + ): Result = runCatching { getClient(userId = userId) .vault() .attachments() - .encryptBuffer( + .encryptFile( cipher = cipher, attachment = attachmentView, - buffer = fileBuffer, + decryptedFilePath = decryptedFilePath, + encryptedFilePath = encryptedFilePath, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManager.kt index 07dcb5351..829b8e40c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManager.kt @@ -17,9 +17,9 @@ interface FileManager { val filesDirectory: String /** - * Deletes a [file] from the system. + * Deletes [files] from disk. */ - suspend fun deleteFile(file: File) + suspend fun delete(vararg files: File) /** * Downloads a file temporarily to cache from [url]. A successful [DownloadResult] will contain diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt index 56faf1771..298c1678a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt @@ -32,9 +32,9 @@ class FileManagerImpl( override val filesDirectory: String get() = context.filesDir.absolutePath - override suspend fun deleteFile(file: File) { + override suspend fun delete(vararg files: File) { withContext(dispatcherManager.io) { - file.delete() + files.forEach { it.delete() } } } 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 00a5ca452..10c473d7c 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 @@ -871,6 +871,7 @@ class VaultRepositoryImpl( ) } + @Suppress("LongMethod") override suspend fun createAttachment( cipherId: String, cipherView: CipherView, @@ -894,34 +895,52 @@ class VaultRepositoryImpl( ) .flatMap { cipher -> fileManager - .uriToByteArray(fileUri = fileUri) - .flatMap { - vaultSdkSource.encryptAttachment( - userId = userId, - cipher = cipher, - attachmentView = attachmentView, - fileBuffer = it, - ) - } - } - .flatMap { attachmentEncryptResult -> - 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(attachmentEncryptResult.attachment.fileName), - key = requireNotNull(attachmentEncryptResult.attachment.key), - fileSize = requireNotNull(attachmentEncryptResult.attachment.size), - ), - ) - .flatMap { attachmentJsonResponse -> - ciphersService.uploadAttachment( - attachmentJsonResponse = attachmentJsonResponse, - encryptedFile = attachmentEncryptResult.contents, - ) + .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) } @@ -1658,7 +1677,7 @@ class VaultRepositoryImpl( ) .also { // Delete encrypted file once it has been uploaded. - fileManager.deleteFile(encryptedFile) + fileManager.delete(encryptedFile) } .map { CreateSendJsonResponse.Success(it) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 3158683e2..6f45be411 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -353,7 +353,7 @@ class VaultItemViewModel @Inject constructor( private fun handleNoAttachmentFileLocationReceive() { viewModelScope.launch { - temporaryAttachmentData?.let { fileManager.deleteFile(it) } + temporaryAttachmentData?.let { fileManager.delete(it) } } mutableStateFlow.update { @@ -932,7 +932,7 @@ class VaultItemViewModel @Inject constructor( action: VaultItemAction.Internal.AttachmentFinishedSavingToDisk, ) { viewModelScope.launch { - fileManager.deleteFile(action.file) + fileManager.delete(action.file) } if (action.isSaved) { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/AttachmentEncryptResultUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/AttachmentEncryptResultUtil.kt deleted file mode 100644 index f6db78f19..000000000 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/AttachmentEncryptResultUtil.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.x8bit.bitwarden.data.vault.datasource.network.model - -import com.bitwarden.core.AttachmentEncryptResult -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment - -/** - * Create a mock [AttachmentEncryptResult] with a given [number]. - */ -fun createMockAttachmentEncryptResult(number: Int): AttachmentEncryptResult = - AttachmentEncryptResult( - attachment = createMockSdkAttachment(number = 1), - contents = byteArrayOf(number.toByte()), - ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt index 78c7644ae..4736c3a32 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import retrofit2.create +import java.io.File import java.time.Clock import java.time.Instant import java.time.ZoneOffset @@ -103,7 +104,7 @@ class CiphersServiceTest : BaseServiceTest() { number = 1, fileUploadType = FileUploadType.AZURE, ) - val encryptedFile = byteArrayOf() + val encryptedFile = File.createTempFile("mockFile", "temp") server.enqueue(MockResponse().setResponseCode(201)) val result = ciphersService.uploadAttachment( @@ -121,7 +122,7 @@ class CiphersServiceTest : BaseServiceTest() { number = 1, fileUploadType = FileUploadType.DIRECT, ) - val encryptedFile = byteArrayOf() + val encryptedFile = File.createTempFile("mockFile", "temp") server.enqueue(MockResponse().setResponseCode(201)) val result = ciphersService.uploadAttachment( 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 d659e2d53..ab2d9f9f6 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 @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk import com.bitwarden.core.Attachment -import com.bitwarden.core.AttachmentEncryptResult import com.bitwarden.core.AttachmentView import com.bitwarden.core.Cipher import com.bitwarden.core.CipherListView @@ -589,15 +588,15 @@ class VaultSdkSourceTest { fun `encryptAttachment should call SDK and return correct data wrapped in a Result`() = runBlocking { val userId = "userId" - val expectedResult = mockk() + val expectedResult = mockk() val mockCipher = mockk() val mockAttachmentView = mockk() - val fileBuffer = byteArrayOf(1, 2) coEvery { - clientVault.attachments().encryptBuffer( + clientVault.attachments().encryptFile( cipher = mockCipher, attachment = mockAttachmentView, - buffer = fileBuffer, + decryptedFilePath = "", + encryptedFilePath = "", ) } returns expectedResult @@ -605,15 +604,17 @@ class VaultSdkSourceTest { userId = userId, cipher = mockCipher, attachmentView = mockAttachmentView, - fileBuffer = fileBuffer, + decryptedFilePath = "", + encryptedFilePath = "", ) assertEquals(expectedResult.asSuccess(), result) coVerify { - clientVault.attachments().encryptBuffer( + clientVault.attachments().encryptFile( cipher = mockCipher, attachment = mockAttachmentView, - buffer = fileBuffer, + decryptedFilePath = "", + encryptedFilePath = "", ) } } 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 426a224d4..729db269a 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 @@ -49,7 +49,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherColle 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 import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest @@ -74,6 +73,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAttachmentV 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 @@ -158,7 +158,9 @@ class VaultRepositoryTest { private val userLogoutManager: UserLogoutManager = mockk { every { logout(any(), any()) } just runs } - private val fileManager: FileManager = mockk() + private val fileManager: FileManager = mockk { + coEvery { delete(*anyVararg()) } just runs + } private val fakeAuthDiskSource = FakeAuthDiskSource() private val settingsDiskSource = mockk { every { getLastSyncTime(userId = any()) } returns clock.instant() @@ -2692,7 +2694,6 @@ class VaultRepositoryTest { } returns mockSendView.asSuccess() every { fileManager.filesDirectory } returns "mockFilesDirectory" coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess() - coEvery { fileManager.deleteFile(any()) } returns Unit coEvery { vaultSdkSource.encryptFile( userId = userId, @@ -2768,7 +2769,6 @@ class VaultRepositoryTest { } returns mockSdkSend.asSuccess() every { fileManager.filesDirectory } returns "mockFilesDirectory" - coEvery { fileManager.deleteFile(any()) } returns Unit coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess() coEvery { vaultSdkSource.encryptFile( @@ -3392,6 +3392,7 @@ class VaultRepositoryTest { 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( @@ -3400,19 +3401,19 @@ class VaultRepositoryTest { url = null, key = null, ) - val mockByteArray = byteArrayOf(1, 2) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) } returns mockCipher.asSuccess() coEvery { - fileManager.uriToByteArray(fileUri = mockUri) - } returns mockByteArray.asSuccess() + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() coEvery { vaultSdkSource.encryptAttachment( userId = userId, cipher = mockCipher, attachmentView = mockAttachmentView, - fileBuffer = mockByteArray, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", ) } returns Throwable("Fail").asFailure() @@ -3443,7 +3444,7 @@ class VaultRepositoryTest { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) } returns mockCipher.asSuccess() coEvery { - fileManager.uriToByteArray(fileUri = mockUri) + fileManager.writeUriToCache(fileUri = mockUri) } returns Throwable("Fail").asFailure() val result = vaultRepository.createAttachment( @@ -3475,22 +3476,23 @@ class VaultRepositoryTest { url = null, key = null, ) - val mockByteArray = byteArrayOf(1, 2) - val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) + val mockFile = File.createTempFile("mockFile", "temp") + val mockAttachment = createMockSdkAttachment(number = 1) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) } returns mockCipher.asSuccess() coEvery { - fileManager.uriToByteArray(fileUri = mockUri) - } returns mockByteArray.asSuccess() + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() coEvery { vaultSdkSource.encryptAttachment( userId = userId, cipher = mockCipher, attachmentView = mockAttachmentView, - fileBuffer = mockByteArray, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", ) - } returns mockAttachmentEncryptResult.asSuccess() + } returns mockAttachment.asSuccess() coEvery { ciphersService.createAttachment( cipherId = cipherId, @@ -3531,23 +3533,24 @@ class VaultRepositoryTest { url = null, key = null, ) - val mockByteArray = byteArrayOf(1, 2) - val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) + 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.uriToByteArray(fileUri = mockUri) - } returns mockByteArray.asSuccess() + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() coEvery { vaultSdkSource.encryptAttachment( userId = userId, cipher = mockCipher, attachmentView = mockAttachmentView, - fileBuffer = mockByteArray, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", ) - } returns mockAttachmentEncryptResult.asSuccess() + } returns mockAttachment.asSuccess() coEvery { ciphersService.createAttachment( cipherId = cipherId, @@ -3561,7 +3564,7 @@ class VaultRepositoryTest { coEvery { ciphersService.uploadAttachment( attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = mockAttachmentEncryptResult.contents, + encryptedFile = File("${mockFile.absoluteFile}.enc"), ) } returns Throwable("Fail").asFailure() @@ -3594,8 +3597,8 @@ class VaultRepositoryTest { url = null, key = null, ) - val mockByteArray = byteArrayOf(1, 2) - val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) + 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( @@ -3605,16 +3608,17 @@ class VaultRepositoryTest { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) } returns mockCipher.asSuccess() coEvery { - fileManager.uriToByteArray(fileUri = mockUri) - } returns mockByteArray.asSuccess() + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() coEvery { vaultSdkSource.encryptAttachment( userId = userId, cipher = mockCipher, attachmentView = mockAttachmentView, - fileBuffer = mockByteArray, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", ) - } returns mockAttachmentEncryptResult.asSuccess() + } returns mockAttachment.asSuccess() coEvery { ciphersService.createAttachment( cipherId = cipherId, @@ -3628,7 +3632,7 @@ class VaultRepositoryTest { coEvery { ciphersService.uploadAttachment( attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = mockAttachmentEncryptResult.contents, + encryptedFile = File("${mockFile.absoluteFile}.enc"), ) } returns mockCipherResponse.asSuccess() coEvery { @@ -3670,8 +3674,8 @@ class VaultRepositoryTest { url = null, key = null, ) - val mockByteArray = byteArrayOf(1, 2) - val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) + 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( @@ -3681,16 +3685,17 @@ class VaultRepositoryTest { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) } returns mockCipher.asSuccess() coEvery { - fileManager.uriToByteArray(fileUri = mockUri) - } returns mockByteArray.asSuccess() + fileManager.writeUriToCache(fileUri = mockUri) + } returns mockFile.asSuccess() coEvery { vaultSdkSource.encryptAttachment( userId = userId, cipher = mockCipher, attachmentView = mockAttachmentView, - fileBuffer = mockByteArray, + decryptedFilePath = mockFile.absolutePath, + encryptedFilePath = "${mockFile.absolutePath}.enc", ) - } returns mockAttachmentEncryptResult.asSuccess() + } returns mockAttachment.asSuccess() coEvery { ciphersService.createAttachment( cipherId = cipherId, @@ -3704,7 +3709,7 @@ class VaultRepositoryTest { coEvery { ciphersService.uploadAttachment( attachmentJsonResponse = mockAttachmentJsonResponse, - encryptedFile = mockAttachmentEncryptResult.contents, + encryptedFile = File("${mockFile.absolutePath}.enc"), ) } returns mockCipherResponse.asSuccess() coEvery { @@ -3728,6 +3733,151 @@ class VaultRepositoryTest { 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 diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 430d78719..8b5789cb8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -1399,7 +1399,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file) coEvery { - mockFileManager.deleteFile(any()) + mockFileManager.delete(any()) } just runs val uri = mockk() @@ -1424,7 +1424,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { } coVerify(exactly = 1) { - mockFileManager.deleteFile(file) + mockFileManager.delete(file) mockFileManager.fileToUri(uri, file) } } @@ -1437,7 +1437,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file) coEvery { - mockFileManager.deleteFile(any()) + mockFileManager.delete(any()) } just runs val uri = mockk() @@ -1466,7 +1466,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { } coVerify(exactly = 1) { - mockFileManager.deleteFile(file) + mockFileManager.delete(file) mockFileManager.fileToUri(uri, file) } } @@ -1477,7 +1477,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file) coEvery { - mockFileManager.deleteFile(any()) + mockFileManager.delete(any()) } just runs viewModel.trySendAction(VaultItemAction.Common.NoAttachmentFileLocationReceive) @@ -1491,7 +1491,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) - coVerify { mockFileManager.deleteFile(file) } + coVerify { mockFileManager.delete(file) } } }