[BIT-2275] Fix OutOfMemoryException when saving attachments (#1418)

This commit is contained in:
Patrick Honkonen 2024-06-04 12:17:24 -04:00 committed by Álison Fernandes
parent a8f7c576fb
commit 51d65f602d
13 changed files with 274 additions and 112 deletions

View file

@ -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.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest 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.UpdateCipherResponseJson
import java.io.File
/** /**
* Provides an API for querying ciphers endpoints. * Provides an API for querying ciphers endpoints.
@ -31,7 +32,7 @@ interface CiphersService {
*/ */
suspend fun uploadAttachment( suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse, attachmentJsonResponse: AttachmentJsonResponse,
encryptedFile: ByteArray, encryptedFile: File,
): Result<SyncResponseJson.Cipher> ): Result<SyncResponseJson.Cipher>
/** /**

View file

@ -17,7 +17,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherRespo
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.time.Clock import java.time.Clock
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -48,7 +49,7 @@ class CiphersServiceImpl(
override suspend fun uploadAttachment( override suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse, attachmentJsonResponse: AttachmentJsonResponse,
encryptedFile: ByteArray, encryptedFile: File,
): Result<SyncResponseJson.Cipher> { ): Result<SyncResponseJson.Cipher> {
val cipher = attachmentJsonResponse.cipherResponse val cipher = attachmentJsonResponse.cipherResponse
return when (attachmentJsonResponse.fileUploadType) { return when (attachmentJsonResponse.fileUploadType) {
@ -62,7 +63,7 @@ class CiphersServiceImpl(
) )
.addPart( .addPart(
part = MultipartBody.Part.createFormData( part = MultipartBody.Part.createFormData(
body = encryptedFile.toRequestBody( body = encryptedFile.asRequestBody(
contentType = "application/octet-stream".toMediaType(), contentType = "application/octet-stream".toMediaType(),
), ),
name = "data", name = "data",
@ -83,7 +84,7 @@ class CiphersServiceImpl(
.RFC_1123_DATE_TIME .RFC_1123_DATE_TIME
.format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)), .format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)),
version = attachmentJsonResponse.url.toUri().getQueryParameter("sv"), version = attachmentJsonResponse.url.toUri().getQueryParameter("sv"),
body = encryptedFile.toRequestBody(), body = encryptedFile.asRequestBody(),
) )
} }
} }

View file

@ -137,8 +137,9 @@ interface VaultSdkSource {
userId: String, userId: String,
cipher: Cipher, cipher: Cipher,
attachmentView: AttachmentView, attachmentView: AttachmentView,
fileBuffer: ByteArray, decryptedFilePath: String,
): Result<AttachmentEncryptResult> encryptedFilePath: String,
): Result<Attachment>
/** /**
* Encrypts a [CipherView] for the user with the given [userId], returning a [Cipher] wrapped * Encrypts a [CipherView] for the user with the given [userId], returning a [Cipher] wrapped

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Attachment import com.bitwarden.core.Attachment
import com.bitwarden.core.AttachmentEncryptResult
import com.bitwarden.core.AttachmentView import com.bitwarden.core.AttachmentView
import com.bitwarden.core.Cipher import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherListView import com.bitwarden.core.CipherListView
@ -183,16 +182,18 @@ class VaultSdkSourceImpl(
userId: String, userId: String,
cipher: Cipher, cipher: Cipher,
attachmentView: AttachmentView, attachmentView: AttachmentView,
fileBuffer: ByteArray, decryptedFilePath: String,
): Result<AttachmentEncryptResult> = encryptedFilePath: String,
): Result<Attachment> =
runCatching { runCatching {
getClient(userId = userId) getClient(userId = userId)
.vault() .vault()
.attachments() .attachments()
.encryptBuffer( .encryptFile(
cipher = cipher, cipher = cipher,
attachment = attachmentView, attachment = attachmentView,
buffer = fileBuffer, decryptedFilePath = decryptedFilePath,
encryptedFilePath = encryptedFilePath,
) )
} }

View file

@ -17,9 +17,9 @@ interface FileManager {
val filesDirectory: String 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 * Downloads a file temporarily to cache from [url]. A successful [DownloadResult] will contain

View file

@ -32,9 +32,9 @@ class FileManagerImpl(
override val filesDirectory: String override val filesDirectory: String
get() = context.filesDir.absolutePath get() = context.filesDir.absolutePath
override suspend fun deleteFile(file: File) { override suspend fun delete(vararg files: File) {
withContext(dispatcherManager.io) { withContext(dispatcherManager.io) {
file.delete() files.forEach { it.delete() }
} }
} }

View file

@ -871,6 +871,7 @@ class VaultRepositoryImpl(
) )
} }
@Suppress("LongMethod")
override suspend fun createAttachment( override suspend fun createAttachment(
cipherId: String, cipherId: String,
cipherView: CipherView, cipherView: CipherView,
@ -894,34 +895,52 @@ class VaultRepositoryImpl(
) )
.flatMap { cipher -> .flatMap { cipher ->
fileManager fileManager
.uriToByteArray(fileUri = fileUri) .writeUriToCache(fileUri = fileUri)
.flatMap { .flatMap { cacheFile ->
vaultSdkSource.encryptAttachment( vaultSdkSource
userId = userId, .encryptAttachment(
cipher = cipher, userId = userId,
attachmentView = attachmentView, cipher = cipher,
fileBuffer = it, attachmentView = attachmentView,
) decryptedFilePath = cacheFile.absolutePath,
} encryptedFilePath = "${cacheFile.absolutePath}.enc",
} )
.flatMap { attachmentEncryptResult -> .flatMap { attachment ->
ciphersService ciphersService
.createAttachment( .createAttachment(
cipherId = cipherId, cipherId = cipherId,
body = AttachmentJsonRequest( body = AttachmentJsonRequest(
// We know these values are present because // We know these values are present because
// - the filename/size are passed into the function // - the filename/size are passed into the function
// - the SDK call fills in the key // - the SDK call fills in the key
fileName = requireNotNull(attachmentEncryptResult.attachment.fileName), fileName = requireNotNull(attachment.fileName),
key = requireNotNull(attachmentEncryptResult.attachment.key), key = requireNotNull(attachment.key),
fileSize = requireNotNull(attachmentEncryptResult.attachment.size), fileSize = requireNotNull(attachment.size),
), ),
) )
.flatMap { attachmentJsonResponse -> .flatMap { attachmentJsonResponse ->
ciphersService.uploadAttachment( val encryptedFile = File("${cacheFile.absolutePath}.enc")
attachmentJsonResponse = attachmentJsonResponse, ciphersService
encryptedFile = attachmentEncryptResult.contents, .uploadAttachment(
) attachmentJsonResponse = attachmentJsonResponse,
encryptedFile = encryptedFile,
)
.onSuccess {
fileManager
.delete(
cacheFile,
encryptedFile,
)
}
.onFailure {
fileManager
.delete(
cacheFile,
encryptedFile,
)
}
}
}
} }
} }
.map { it.copy(collectionIds = cipherView.collectionIds) } .map { it.copy(collectionIds = cipherView.collectionIds) }
@ -1658,7 +1677,7 @@ class VaultRepositoryImpl(
) )
.also { .also {
// Delete encrypted file once it has been uploaded. // Delete encrypted file once it has been uploaded.
fileManager.deleteFile(encryptedFile) fileManager.delete(encryptedFile)
} }
.map { CreateSendJsonResponse.Success(it) } .map { CreateSendJsonResponse.Success(it) }
} }

View file

@ -353,7 +353,7 @@ class VaultItemViewModel @Inject constructor(
private fun handleNoAttachmentFileLocationReceive() { private fun handleNoAttachmentFileLocationReceive() {
viewModelScope.launch { viewModelScope.launch {
temporaryAttachmentData?.let { fileManager.deleteFile(it) } temporaryAttachmentData?.let { fileManager.delete(it) }
} }
mutableStateFlow.update { mutableStateFlow.update {
@ -932,7 +932,7 @@ class VaultItemViewModel @Inject constructor(
action: VaultItemAction.Internal.AttachmentFinishedSavingToDisk, action: VaultItemAction.Internal.AttachmentFinishedSavingToDisk,
) { ) {
viewModelScope.launch { viewModelScope.launch {
fileManager.deleteFile(action.file) fileManager.delete(action.file)
} }
if (action.isSaved) { if (action.isSaved) {

View file

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

View file

@ -26,6 +26,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import retrofit2.create import retrofit2.create
import java.io.File
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset import java.time.ZoneOffset
@ -103,7 +104,7 @@ class CiphersServiceTest : BaseServiceTest() {
number = 1, number = 1,
fileUploadType = FileUploadType.AZURE, fileUploadType = FileUploadType.AZURE,
) )
val encryptedFile = byteArrayOf() val encryptedFile = File.createTempFile("mockFile", "temp")
server.enqueue(MockResponse().setResponseCode(201)) server.enqueue(MockResponse().setResponseCode(201))
val result = ciphersService.uploadAttachment( val result = ciphersService.uploadAttachment(
@ -121,7 +122,7 @@ class CiphersServiceTest : BaseServiceTest() {
number = 1, number = 1,
fileUploadType = FileUploadType.DIRECT, fileUploadType = FileUploadType.DIRECT,
) )
val encryptedFile = byteArrayOf() val encryptedFile = File.createTempFile("mockFile", "temp")
server.enqueue(MockResponse().setResponseCode(201)) server.enqueue(MockResponse().setResponseCode(201))
val result = ciphersService.uploadAttachment( val result = ciphersService.uploadAttachment(

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Attachment import com.bitwarden.core.Attachment
import com.bitwarden.core.AttachmentEncryptResult
import com.bitwarden.core.AttachmentView import com.bitwarden.core.AttachmentView
import com.bitwarden.core.Cipher import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherListView import com.bitwarden.core.CipherListView
@ -589,15 +588,15 @@ class VaultSdkSourceTest {
fun `encryptAttachment should call SDK and return correct data wrapped in a Result`() = fun `encryptAttachment should call SDK and return correct data wrapped in a Result`() =
runBlocking { runBlocking {
val userId = "userId" val userId = "userId"
val expectedResult = mockk<AttachmentEncryptResult>() val expectedResult = mockk<Attachment>()
val mockCipher = mockk<Cipher>() val mockCipher = mockk<Cipher>()
val mockAttachmentView = mockk<AttachmentView>() val mockAttachmentView = mockk<AttachmentView>()
val fileBuffer = byteArrayOf(1, 2)
coEvery { coEvery {
clientVault.attachments().encryptBuffer( clientVault.attachments().encryptFile(
cipher = mockCipher, cipher = mockCipher,
attachment = mockAttachmentView, attachment = mockAttachmentView,
buffer = fileBuffer, decryptedFilePath = "",
encryptedFilePath = "",
) )
} returns expectedResult } returns expectedResult
@ -605,15 +604,17 @@ class VaultSdkSourceTest {
userId = userId, userId = userId,
cipher = mockCipher, cipher = mockCipher,
attachmentView = mockAttachmentView, attachmentView = mockAttachmentView,
fileBuffer = fileBuffer, decryptedFilePath = "",
encryptedFilePath = "",
) )
assertEquals(expectedResult.asSuccess(), result) assertEquals(expectedResult.asSuccess(), result)
coVerify { coVerify {
clientVault.attachments().encryptBuffer( clientVault.attachments().encryptFile(
cipher = mockCipher, cipher = mockCipher,
attachment = mockAttachmentView, attachment = mockAttachmentView,
buffer = fileBuffer, decryptedFilePath = "",
encryptedFilePath = "",
) )
} }
} }

View file

@ -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.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson 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.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.createMockAttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher 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.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.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView 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.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.createMockSdkCipher
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCollection 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.createMockSdkFolder
@ -158,7 +158,9 @@ class VaultRepositoryTest {
private val userLogoutManager: UserLogoutManager = mockk { private val userLogoutManager: UserLogoutManager = mockk {
every { logout(any(), any()) } just runs 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 fakeAuthDiskSource = FakeAuthDiskSource()
private val settingsDiskSource = mockk<SettingsDiskSource> { private val settingsDiskSource = mockk<SettingsDiskSource> {
every { getLastSyncTime(userId = any()) } returns clock.instant() every { getLastSyncTime(userId = any()) } returns clock.instant()
@ -2692,7 +2694,6 @@ class VaultRepositoryTest {
} returns mockSendView.asSuccess() } returns mockSendView.asSuccess()
every { fileManager.filesDirectory } returns "mockFilesDirectory" every { fileManager.filesDirectory } returns "mockFilesDirectory"
coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess() coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess()
coEvery { fileManager.deleteFile(any()) } returns Unit
coEvery { coEvery {
vaultSdkSource.encryptFile( vaultSdkSource.encryptFile(
userId = userId, userId = userId,
@ -2768,7 +2769,6 @@ class VaultRepositoryTest {
} returns mockSdkSend.asSuccess() } returns mockSdkSend.asSuccess()
every { fileManager.filesDirectory } returns "mockFilesDirectory" every { fileManager.filesDirectory } returns "mockFilesDirectory"
coEvery { fileManager.deleteFile(any()) } returns Unit
coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess() coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess()
coEvery { coEvery {
vaultSdkSource.encryptFile( vaultSdkSource.encryptFile(
@ -3392,6 +3392,7 @@ class VaultRepositoryTest {
val mockUri = setupMockUri(url = "www.test.com") val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1) val mockCipherView = createMockCipherView(number = 1)
val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockCipher = createMockSdkCipher(number = 1, clock = clock)
val mockFile = File.createTempFile("mockFile", "temp")
val mockFileName = "mockFileName-1" val mockFileName = "mockFileName-1"
val mockFileSize = "1" val mockFileSize = "1"
val mockAttachmentView = createMockAttachmentView(number = 1).copy( val mockAttachmentView = createMockAttachmentView(number = 1).copy(
@ -3400,19 +3401,19 @@ class VaultRepositoryTest {
url = null, url = null,
key = null, key = null,
) )
val mockByteArray = byteArrayOf(1, 2)
coEvery { coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess() } returns mockCipher.asSuccess()
coEvery { coEvery {
fileManager.uriToByteArray(fileUri = mockUri) fileManager.writeUriToCache(fileUri = mockUri)
} returns mockByteArray.asSuccess() } returns mockFile.asSuccess()
coEvery { coEvery {
vaultSdkSource.encryptAttachment( vaultSdkSource.encryptAttachment(
userId = userId, userId = userId,
cipher = mockCipher, cipher = mockCipher,
attachmentView = mockAttachmentView, attachmentView = mockAttachmentView,
fileBuffer = mockByteArray, decryptedFilePath = mockFile.absolutePath,
encryptedFilePath = "${mockFile.absolutePath}.enc",
) )
} returns Throwable("Fail").asFailure() } returns Throwable("Fail").asFailure()
@ -3443,7 +3444,7 @@ class VaultRepositoryTest {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess() } returns mockCipher.asSuccess()
coEvery { coEvery {
fileManager.uriToByteArray(fileUri = mockUri) fileManager.writeUriToCache(fileUri = mockUri)
} returns Throwable("Fail").asFailure() } returns Throwable("Fail").asFailure()
val result = vaultRepository.createAttachment( val result = vaultRepository.createAttachment(
@ -3475,22 +3476,23 @@ class VaultRepositoryTest {
url = null, url = null,
key = null, key = null,
) )
val mockByteArray = byteArrayOf(1, 2) val mockFile = File.createTempFile("mockFile", "temp")
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) val mockAttachment = createMockSdkAttachment(number = 1)
coEvery { coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess() } returns mockCipher.asSuccess()
coEvery { coEvery {
fileManager.uriToByteArray(fileUri = mockUri) fileManager.writeUriToCache(fileUri = mockUri)
} returns mockByteArray.asSuccess() } returns mockFile.asSuccess()
coEvery { coEvery {
vaultSdkSource.encryptAttachment( vaultSdkSource.encryptAttachment(
userId = userId, userId = userId,
cipher = mockCipher, cipher = mockCipher,
attachmentView = mockAttachmentView, attachmentView = mockAttachmentView,
fileBuffer = mockByteArray, decryptedFilePath = mockFile.absolutePath,
encryptedFilePath = "${mockFile.absolutePath}.enc",
) )
} returns mockAttachmentEncryptResult.asSuccess() } returns mockAttachment.asSuccess()
coEvery { coEvery {
ciphersService.createAttachment( ciphersService.createAttachment(
cipherId = cipherId, cipherId = cipherId,
@ -3531,23 +3533,24 @@ class VaultRepositoryTest {
url = null, url = null,
key = null, key = null,
) )
val mockByteArray = byteArrayOf(1, 2) val mockFile = File.createTempFile("mockFile", "temp")
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) val mockAttachment = createMockSdkAttachment(number = 1)
val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1)
coEvery { coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess() } returns mockCipher.asSuccess()
coEvery { coEvery {
fileManager.uriToByteArray(fileUri = mockUri) fileManager.writeUriToCache(fileUri = mockUri)
} returns mockByteArray.asSuccess() } returns mockFile.asSuccess()
coEvery { coEvery {
vaultSdkSource.encryptAttachment( vaultSdkSource.encryptAttachment(
userId = userId, userId = userId,
cipher = mockCipher, cipher = mockCipher,
attachmentView = mockAttachmentView, attachmentView = mockAttachmentView,
fileBuffer = mockByteArray, decryptedFilePath = mockFile.absolutePath,
encryptedFilePath = "${mockFile.absolutePath}.enc",
) )
} returns mockAttachmentEncryptResult.asSuccess() } returns mockAttachment.asSuccess()
coEvery { coEvery {
ciphersService.createAttachment( ciphersService.createAttachment(
cipherId = cipherId, cipherId = cipherId,
@ -3561,7 +3564,7 @@ class VaultRepositoryTest {
coEvery { coEvery {
ciphersService.uploadAttachment( ciphersService.uploadAttachment(
attachmentJsonResponse = mockAttachmentJsonResponse, attachmentJsonResponse = mockAttachmentJsonResponse,
encryptedFile = mockAttachmentEncryptResult.contents, encryptedFile = File("${mockFile.absoluteFile}.enc"),
) )
} returns Throwable("Fail").asFailure() } returns Throwable("Fail").asFailure()
@ -3594,8 +3597,8 @@ class VaultRepositoryTest {
url = null, url = null,
key = null, key = null,
) )
val mockByteArray = byteArrayOf(1, 2) val mockFile = File.createTempFile("mockFile", "temp")
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) val mockAttachment = createMockSdkAttachment(number = 1)
val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1)
val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null)
val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( val mockUpdatedCipherResponse = createMockCipher(number = 1).copy(
@ -3605,16 +3608,17 @@ class VaultRepositoryTest {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess() } returns mockCipher.asSuccess()
coEvery { coEvery {
fileManager.uriToByteArray(fileUri = mockUri) fileManager.writeUriToCache(fileUri = mockUri)
} returns mockByteArray.asSuccess() } returns mockFile.asSuccess()
coEvery { coEvery {
vaultSdkSource.encryptAttachment( vaultSdkSource.encryptAttachment(
userId = userId, userId = userId,
cipher = mockCipher, cipher = mockCipher,
attachmentView = mockAttachmentView, attachmentView = mockAttachmentView,
fileBuffer = mockByteArray, decryptedFilePath = mockFile.absolutePath,
encryptedFilePath = "${mockFile.absolutePath}.enc",
) )
} returns mockAttachmentEncryptResult.asSuccess() } returns mockAttachment.asSuccess()
coEvery { coEvery {
ciphersService.createAttachment( ciphersService.createAttachment(
cipherId = cipherId, cipherId = cipherId,
@ -3628,7 +3632,7 @@ class VaultRepositoryTest {
coEvery { coEvery {
ciphersService.uploadAttachment( ciphersService.uploadAttachment(
attachmentJsonResponse = mockAttachmentJsonResponse, attachmentJsonResponse = mockAttachmentJsonResponse,
encryptedFile = mockAttachmentEncryptResult.contents, encryptedFile = File("${mockFile.absoluteFile}.enc"),
) )
} returns mockCipherResponse.asSuccess() } returns mockCipherResponse.asSuccess()
coEvery { coEvery {
@ -3670,8 +3674,8 @@ class VaultRepositoryTest {
url = null, url = null,
key = null, key = null,
) )
val mockByteArray = byteArrayOf(1, 2) val mockFile = File.createTempFile("mockFile", "temp")
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1) val mockAttachment = createMockSdkAttachment(number = 1)
val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1) val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1)
val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null) val mockCipherResponse = createMockCipher(number = 1).copy(collectionIds = null)
val mockUpdatedCipherResponse = createMockCipher(number = 1).copy( val mockUpdatedCipherResponse = createMockCipher(number = 1).copy(
@ -3681,16 +3685,17 @@ class VaultRepositoryTest {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess() } returns mockCipher.asSuccess()
coEvery { coEvery {
fileManager.uriToByteArray(fileUri = mockUri) fileManager.writeUriToCache(fileUri = mockUri)
} returns mockByteArray.asSuccess() } returns mockFile.asSuccess()
coEvery { coEvery {
vaultSdkSource.encryptAttachment( vaultSdkSource.encryptAttachment(
userId = userId, userId = userId,
cipher = mockCipher, cipher = mockCipher,
attachmentView = mockAttachmentView, attachmentView = mockAttachmentView,
fileBuffer = mockByteArray, decryptedFilePath = mockFile.absolutePath,
encryptedFilePath = "${mockFile.absolutePath}.enc",
) )
} returns mockAttachmentEncryptResult.asSuccess() } returns mockAttachment.asSuccess()
coEvery { coEvery {
ciphersService.createAttachment( ciphersService.createAttachment(
cipherId = cipherId, cipherId = cipherId,
@ -3704,7 +3709,7 @@ class VaultRepositoryTest {
coEvery { coEvery {
ciphersService.uploadAttachment( ciphersService.uploadAttachment(
attachmentJsonResponse = mockAttachmentJsonResponse, attachmentJsonResponse = mockAttachmentJsonResponse,
encryptedFile = mockAttachmentEncryptResult.contents, encryptedFile = File("${mockFile.absolutePath}.enc"),
) )
} returns mockCipherResponse.asSuccess() } returns mockCipherResponse.asSuccess()
coEvery { coEvery {
@ -3728,6 +3733,151 @@ class VaultRepositoryTest {
assertEquals(CreateAttachmentResult.Success(mockCipherView), result) 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 @Test
fun `downloadAttachment with missing attachment should return Failure`() = runTest { fun `downloadAttachment with missing attachment should return Failure`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE

View file

@ -1399,7 +1399,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file) val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file)
coEvery { coEvery {
mockFileManager.deleteFile(any()) mockFileManager.delete(any())
} just runs } just runs
val uri = mockk<Uri>() val uri = mockk<Uri>()
@ -1424,7 +1424,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
mockFileManager.deleteFile(file) mockFileManager.delete(file)
mockFileManager.fileToUri(uri, file) mockFileManager.fileToUri(uri, file)
} }
} }
@ -1437,7 +1437,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file) val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file)
coEvery { coEvery {
mockFileManager.deleteFile(any()) mockFileManager.delete(any())
} just runs } just runs
val uri = mockk<Uri>() val uri = mockk<Uri>()
@ -1466,7 +1466,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
mockFileManager.deleteFile(file) mockFileManager.delete(file)
mockFileManager.fileToUri(uri, file) mockFileManager.fileToUri(uri, file)
} }
} }
@ -1477,7 +1477,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file) val viewModel = createViewModel(state = DEFAULT_STATE, tempAttachmentFile = file)
coEvery { coEvery {
mockFileManager.deleteFile(any()) mockFileManager.delete(any())
} just runs } just runs
viewModel.trySendAction(VaultItemAction.Common.NoAttachmentFileLocationReceive) viewModel.trySendAction(VaultItemAction.Common.NoAttachmentFileLocationReceive)
@ -1491,7 +1491,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
coVerify { mockFileManager.deleteFile(file) } coVerify { mockFileManager.delete(file) }
} }
} }