From cd236f183f4c9b415d74f418ca685e7489c6841f Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 17 Jan 2024 14:41:18 -0600 Subject: [PATCH] Add underlying support for file sends (#646) --- .../vault/datasource/network/api/AzureApi.kt | 25 ++ .../vault/datasource/network/api/SendsApi.kt | 18 ++ .../network/di/VaultNetworkModule.kt | 9 + .../network/model/SendFileResponseJson.kt | 39 +++ .../network/model/SendJsonRequest.kt | 4 + .../network/service/SendsService.kt | 16 ++ .../network/service/SendsServiceImpl.kt | 57 +++++ .../vault/datasource/sdk/VaultSdkSource.kt | 13 + .../datasource/sdk/VaultSdkSourceImpl.kt | 15 ++ .../data/vault/manager/FileManager.kt | 16 ++ .../data/vault/manager/FileManagerImpl.kt | 36 +++ .../vault/manager/di/VaultManagerModule.kt | 10 + .../data/vault/repository/VaultRepository.kt | 7 +- .../vault/repository/VaultRepositoryImpl.kt | 45 +++- .../repository/di/VaultRepositoryModule.kt | 3 + .../repository/util/VaultSdkSendExtensions.kt | 3 +- .../feature/send/addsend/AddSendViewModel.kt | 5 +- .../network/model/SendJsonRequestUtil.kt | 1 + .../network/model/SyncResponseSendUtil.kt | 7 +- .../network/service/SendsServiceTest.kt | 132 +++++++++- .../datasource/sdk/model/VaultSdkSendUtil.kt | 7 +- .../vault/repository/VaultRepositoryTest.kt | 231 +++++++++++++++--- .../util/VaultSdkSendExtensionsTest.kt | 13 +- .../send/addsend/AddSendViewModelTest.kt | 10 +- 24 files changed, 676 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendFileResponseJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt new file mode 100644 index 0000000000..42adf069a1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.api + +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.PUT +import retrofit2.http.Url + +/** + * Defines raw calls to the Azure API without any authentication applied. + */ +interface AzureApi { + /** + * Attempts to upload an encrypted file to Azure. + */ + @PUT + @Headers("x-ms-blob-type: BlockBlob") + suspend fun uploadAzureBlob( + @Url url: String, + @Header("x-ms-date") date: String, + @Header("x-ms-version") version: String?, + @Body body: RequestBody, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt index 44e1932395..801604e9aa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api import androidx.annotation.Keep +import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson +import okhttp3.MultipartBody import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.POST @@ -21,6 +23,12 @@ interface SendsApi { @POST("sends") suspend fun createSend(@Body body: SendJsonRequest): Result + /** + * Create a file send. + */ + @POST("sends/file/v2") + suspend fun createFileSend(@Body body: SendJsonRequest): Result + /** * Updates a send. */ @@ -30,6 +38,16 @@ interface SendsApi { @Body body: SendJsonRequest, ): Result + /** + * Uploads the file associated with a send. + */ + @POST("sends/{sendId}/file/{fileId}") + suspend fun uploadFile( + @Path("sendId") sendId: String, + @Path("fileId") fileId: String, + @Body body: MultipartBody, + ): Result + /** * Deletes a send. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt index e601747214..5c70f4eb10 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt @@ -13,6 +13,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json import retrofit2.create +import java.time.Clock import javax.inject.Singleton /** @@ -37,9 +38,17 @@ object VaultNetworkModule { fun provideSendsService( retrofits: Retrofits, json: Json, + clock: Clock, ): SendsService = SendsServiceImpl( + azureApi = retrofits + .staticRetrofitBuilder + // This URL will be overridden dynamically + .baseUrl("https://www.bitwaredn.com") + .build() + .create(), sendsApi = retrofits.authenticatedApiRetrofit.create(), json = json, + clock = clock, ) @Provides diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendFileResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendFileResponseJson.kt new file mode 100644 index 0000000000..902716ae5d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendFileResponseJson.kt @@ -0,0 +1,39 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.model + +import androidx.annotation.Keep +import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents the JSON response from creating a new file send. + */ +@Serializable +data class SendFileResponseJson( + @SerialName("url") + val url: String, + + @SerialName("fileUploadType") + val fileUploadType: FileUploadType, + + @SerialName("sendResponse") + val sendResponse: SyncResponseJson.Send, +) { + /** + * Represents the type of file upload that should be used. + */ + @Serializable(FileUploadTypeSerializer::class) + enum class FileUploadType { + @SerialName("0") + DIRECT, + + @SerialName("1") + AZURE, + } +} + +@Keep +private class FileUploadTypeSerializer : + BaseEnumeratedIntSerializer( + SendFileResponseJson.FileUploadType.entries.toTypedArray(), + ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt index c3ce4da392..9d37a0cc28 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt @@ -16,6 +16,7 @@ import java.time.ZonedDateTime * @property expirationDate The date in which the send will expire (nullable). * @property deletionDate The date in which the send will be deleted. * @property file The file associated with this send (nullable). + * @property fileLength The length of the file in bytes (nullable). * @property text The text associated with this send (nullable). * @property password The password protecting this send (nullable). * @property isDisabled Indicate if this send is disabled. @@ -46,6 +47,9 @@ data class SendJsonRequest( @Contextual val deletionDate: ZonedDateTime, + @SerialName("fileLength") + val fileLength: Int?, + @SerialName("file") val file: SyncResponseJson.Send.File?, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt index 23e5d5307f..8cdc0e4d52 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service +import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson @@ -15,6 +16,21 @@ interface SendsService { body: SendJsonRequest, ): Result + /** + * Attempt to create a file send. + */ + suspend fun createFileSend( + body: SendJsonRequest, + ): Result + + /** + * Attempt to upload the given [encryptedFile] associated with the [sendFileResponse]. + */ + suspend fun uploadFile( + sendFileResponse: SendFileResponseJson, + encryptedFile: ByteArray, + ): Result + /** * Attempt to update a send. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt index 4b2470694f..5cdc238f03 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt @@ -1,23 +1,38 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service +import androidx.core.net.toUri import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull +import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi +import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.time.Clock +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter /** * Default implementation of the [SendsService]. */ class SendsServiceImpl( + private val azureApi: AzureApi, private val sendsApi: SendsApi, + private val clock: Clock, private val json: Json, ) : SendsService { override suspend fun createSend(body: SendJsonRequest): Result = sendsApi.createSend(body = body) + override suspend fun createFileSend(body: SendJsonRequest): Result = + sendsApi.createFileSend(body = body) + override suspend fun updateSend( sendId: String, body: SendJsonRequest, @@ -38,6 +53,48 @@ class SendsServiceImpl( ?: throw throwable } + override suspend fun uploadFile( + sendFileResponse: SendFileResponseJson, + encryptedFile: ByteArray, + ): Result { + val send = sendFileResponse.sendResponse + return when (sendFileResponse.fileUploadType) { + SendFileResponseJson.FileUploadType.DIRECT -> { + sendsApi.uploadFile( + sendId = requireNotNull(send.id), + fileId = requireNotNull(send.file?.id), + body = MultipartBody + .Builder( + boundary = "--BWMobileFormBoundary${clock.instant().toEpochMilli()}", + ) + .addPart( + part = MultipartBody.Part.createFormData( + body = encryptedFile.toRequestBody( + contentType = "application/octet-stream".toMediaType(), + ), + name = "data", + filename = send.file?.fileName, + ), + ) + .build(), + ) + } + + SendFileResponseJson.FileUploadType.AZURE -> { + azureApi.uploadAzureBlob( + url = sendFileResponse.url, + date = DateTimeFormatter + .RFC_1123_DATE_TIME + .format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)), + version = sendFileResponse.url.toUri().getQueryParameter("sv"), + body = encryptedFile.toRequestBody(), + ) + } + } + .onFailure { sendsApi.deleteSend(send.id) } + .map { send } + } + override suspend fun deleteSend(sendId: String): Result = sendsApi.deleteSend(sendId = sendId) 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 a1222ae756..44e4fef739 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 @@ -169,6 +169,19 @@ interface VaultSdkSource { sendView: SendView, ): Result + /** + * Encrypts a [ByteArray] file buffer for the user with the given [userId], returning an + * encrypted [ByteArray] wrapped in a [Result]. + * + * This should only be called after a successful call to [initializeCrypto] for the associated + * user. + */ + suspend fun encryptBuffer( + userId: String, + send: Send, + fileBuffer: ByteArray, + ): Result + /** * Decrypts a [Send] for the user with the given [userId], returning a [SendView] 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 fd0d29cb06..5c4da991ad 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 @@ -104,6 +104,21 @@ class VaultSdkSourceImpl( .encrypt(sendView) } + override suspend fun encryptBuffer( + userId: String, + send: Send, + fileBuffer: ByteArray, + ): Result = + runCatching { + getClient(userId = userId) + .vault() + .sends() + .encryptBuffer( + send = send, + buffer = fileBuffer, + ) + } + override suspend fun encryptCipher( userId: String, cipherView: CipherView, 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 new file mode 100644 index 0000000000..1c0810a7ce --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManager.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.vault.manager + +import android.net.Uri +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage + +/** + * Manages reading files. + */ +@OmitFromCoverage +interface FileManager { + + /** + * Reads the [fileUri] into memory and returns the raw [ByteArray] + */ + fun uriToByteArray(fileUri: Uri): ByteArray +} 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 new file mode 100644 index 0000000000..3238a7f2c9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt @@ -0,0 +1,36 @@ +package com.x8bit.bitwarden.data.vault.manager + +import android.content.Context +import android.net.Uri +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import java.io.ByteArrayOutputStream + +/** + * The buffer size to be used when reading from an input stream. + */ +private const val BUFFER_SIZE: Int = 1024 + +/** + * The default implementation of the [FileManager] interface. + */ +@OmitFromCoverage +class FileManagerImpl( + private val context: Context, +) : FileManager { + + override fun uriToByteArray(fileUri: Uri): ByteArray = + context + .contentResolver + .openInputStream(fileUri) + ?.use { inputStream -> + ByteArrayOutputStream().use { outputStream -> + val buffer = ByteArray(BUFFER_SIZE) + var length: Int + while (inputStream.read(buffer).also { length = it } != -1) { + outputStream.write(buffer, 0, length) + } + outputStream.toByteArray() + } + } + ?: byteArrayOf() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt index 6e350aef66..227bacac6a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -1,16 +1,20 @@ package com.x8bit.bitwarden.data.vault.manager.di +import android.content.Context import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.FileManager +import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -21,6 +25,12 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object VaultManagerModule { + @Provides + @Singleton + fun provideFileManager( + @ApplicationContext context: Context, + ): FileManager = FileManagerImpl(context) + @Provides @Singleton fun provideVaultLockManager( 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 38ed50f0e9..7c2a457019 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 @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.data.vault.repository +import android.net.Uri import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView import com.bitwarden.core.FolderView +import com.bitwarden.core.SendType import com.bitwarden.core.SendView import com.bitwarden.crypto.Kdf import com.x8bit.bitwarden.data.platform.repository.model.DataState @@ -159,9 +161,10 @@ interface VaultRepository : VaultLockManager { ): UpdateCipherResult /** - * Attempt to create a send. + * Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a + * [SendView.type] of [SendType.FILE]. */ - suspend fun createSend(sendView: SendView): CreateSendResult + suspend fun createSend(sendView: SendView, fileUri: Uri?): CreateSendResult /** * Attempt to update a send. 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 66a7743e45..86e1154dbf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -1,10 +1,12 @@ package com.x8bit.bitwarden.data.vault.repository +import android.net.Uri import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView import com.bitwarden.core.FolderView import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod +import com.bitwarden.core.SendType import com.bitwarden.core.SendView import com.bitwarden.crypto.Kdf import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource @@ -18,6 +20,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates import com.x8bit.bitwarden.data.platform.repository.util.map import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading +import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson @@ -27,6 +30,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult @@ -80,6 +84,7 @@ class VaultRepositoryImpl( private val vaultDiskSource: VaultDiskSource, private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, + private val fileManager: FileManager, private val vaultLockManager: VaultLockManager, dispatcherManager: DispatcherManager, ) : VaultRepository, @@ -415,14 +420,50 @@ class VaultRepositoryImpl( ) } - override suspend fun createSend(sendView: SendView): CreateSendResult { + override suspend fun createSend( + sendView: SendView, + fileUri: Uri?, + ): CreateSendResult { val userId = requireNotNull(activeUserId) return vaultSdkSource .encryptSend( userId = userId, sendView = sendView, ) - .flatMap { send -> sendsService.createSend(body = send.toEncryptedNetworkSend()) } + .flatMap { send -> + when (send.type) { + SendType.TEXT -> { + sendsService.createSend(body = send.toEncryptedNetworkSend()) + } + + SendType.FILE -> { + val uri = fileUri ?: return@flatMap IllegalArgumentException( + "File URI must be present to create a File Send.", + ) + .asFailure() + vaultSdkSource + .encryptBuffer( + userId = userId, + send = send, + fileBuffer = fileManager.uriToByteArray(fileUri = uri), + ) + .flatMap { encryptedFile -> + sendsService + .createFileSend( + body = send.toEncryptedNetworkSend( + fileLength = encryptedFile.size, + ), + ) + .flatMap { sendFileResponse -> + sendsService.uploadFile( + sendFileResponse = sendFileResponse, + encryptedFile = encryptedFile, + ) + } + } + } + } + } .onSuccess { // Save the send immediately, regardless of whether the decrypt succeeds vaultDiskSource.saveSend(userId = userId, send = it) 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 f242f43099..f034f51613 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 @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl @@ -32,6 +33,7 @@ object VaultRepositoryModule { vaultDiskSource: VaultDiskSource, vaultSdkSource: VaultSdkSource, authDiskSource: AuthDiskSource, + fileManager: FileManager, vaultLockManager: VaultLockManager, dispatcherManager: DispatcherManager, ): VaultRepository = VaultRepositoryImpl( @@ -41,6 +43,7 @@ object VaultRepositoryModule { vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, authDiskSource = authDiskSource, + fileManager = fileManager, vaultLockManager = vaultLockManager, dispatcherManager = dispatcherManager, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensions.kt index d2674f9a40..fe7ce899a4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensions.kt @@ -13,7 +13,7 @@ import java.time.ZonedDateTime /** * Converts a Bitwarden SDK [Send] object to a corresponding [SyncResponseJson.Send] object. */ -fun Send.toEncryptedNetworkSend(): SendJsonRequest = +fun Send.toEncryptedNetworkSend(fileLength: Int? = null): SendJsonRequest = SendJsonRequest( type = type.toNetworkSendType(), name = name, @@ -22,6 +22,7 @@ fun Send.toEncryptedNetworkSend(): SendJsonRequest = maxAccessCount = maxAccessCount?.toInt(), expirationDate = expirationDate?.let { ZonedDateTime.ofInstant(it, ZoneOffset.UTC) }, deletionDate = ZonedDateTime.ofInstant(deletionDate, ZoneOffset.UTC), + fileLength = fileLength, file = file?.toNetworkSendFile(), text = text?.toNetworkSendText(), password = password, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt index 3b31129383..b871bdf685 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt @@ -452,7 +452,10 @@ class AddSendViewModel @Inject constructor( viewModelScope.launch { when (val addSendType = state.addSendType) { AddSendType.AddItem -> { - val result = vaultRepo.createSend(content.toSendView(clock)) + val result = vaultRepo.createSend( + sendView = content.toSendView(clock), + fileUri = null, + ) sendAction(AddSendAction.Internal.CreateSendResultReceive(result)) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt index 0cfa7d8ba2..4ff166c5d1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt @@ -17,6 +17,7 @@ fun createMockSendJsonRequest( maxAccessCount = 1, expirationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), deletionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), + fileLength = 1, file = createMockFile(number), text = createMockText(number), password = "mockPassword-$number", diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseSendUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseSendUtil.kt index 2a748c3bde..5cec0df8a0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseSendUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseSendUtil.kt @@ -2,14 +2,17 @@ package com.x8bit.bitwarden.data.vault.datasource.network.model import java.time.ZonedDateTime -fun createMockSend(number: Int): SyncResponseJson.Send = +fun createMockSend( + number: Int, + type: SendTypeJson = SendTypeJson.FILE, +): SyncResponseJson.Send = SyncResponseJson.Send( accessCount = 1, notes = "mockNotes-$number", revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), maxAccessCount = 1, shouldHideEmail = false, - type = SendTypeJson.FILE, + type = type, accessId = "mockAccessId-$number", password = "mockPassword-$number", file = createMockFile(number = number), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt index 2f7b29a281..6eb656178c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt @@ -1,29 +1,73 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service +import android.net.Uri import com.x8bit.bitwarden.data.platform.base.BaseServiceTest +import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi +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.UpdateSendResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import retrofit2.create +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset class SendsServiceTest : BaseServiceTest() { + private val clock: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, + ) + private val azureApi: AzureApi = retrofit.create() private val sendsApi: SendsApi = retrofit.create() private val sendsService: SendsService = SendsServiceImpl( + azureApi = azureApi, sendsApi = sendsApi, json = json, + clock = clock, ) + @BeforeEach + fun setup() { + mockkStatic(Uri::class) + } + + @AfterEach + fun tearDown() { + unmockkStatic(Uri::class) + } + + @Test + fun `createFileSend should return the correct response`() = runTest { + val response = SendFileResponseJson( + url = "www.test.com", + fileUploadType = SendFileResponseJson.FileUploadType.AZURE, + sendResponse = createMockSend(number = 1, type = SendTypeJson.FILE), + ) + server.enqueue(MockResponse().setBody(CREATE_FILE_SEND_SUCCESS_JSON)) + val result = sendsService.createFileSend( + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.FILE), + ) + assertEquals(response, result.getOrThrow()) + } + @Test fun `createSend should return the correct response`() = runTest { server.enqueue(MockResponse().setBody(CREATE_UPDATE_SEND_SUCCESS_JSON)) val result = sendsService.createSend( - body = createMockSendJsonRequest(number = 1), + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT), ) assertEquals( createMockSend(number = 1), @@ -64,6 +108,46 @@ class SendsServiceTest : BaseServiceTest() { ) } + @Test + fun `uploadFile with Azure uploadFile success should return send`() = runTest { + val url = "www.test.com" + setupMockUri(url = url, queryParams = mapOf("sv" to "2024-04-03")) + val mockSend = createMockSend(number = 1, type = SendTypeJson.FILE) + val sendFileResponse = SendFileResponseJson( + url = url, + fileUploadType = SendFileResponseJson.FileUploadType.AZURE, + sendResponse = mockSend, + ) + val encryptedFile = byteArrayOf() + server.enqueue(MockResponse().setResponseCode(201)) + + val result = sendsService.uploadFile( + sendFileResponse = sendFileResponse, + encryptedFile = encryptedFile, + ) + + assertEquals(mockSend, result.getOrThrow()) + } + + @Test + fun `uploadFile with Direct uploadFile success should return send`() = runTest { + val mockSend = createMockSend(number = 1, type = SendTypeJson.FILE) + val sendFileResponse = SendFileResponseJson( + url = "www.test.com", + fileUploadType = SendFileResponseJson.FileUploadType.DIRECT, + sendResponse = mockSend, + ) + val encryptedFile = byteArrayOf() + server.enqueue(MockResponse().setResponseCode(201)) + + val result = sendsService.uploadFile( + sendFileResponse = sendFileResponse, + encryptedFile = encryptedFile, + ) + + assertEquals(mockSend, result.getOrThrow()) + } + @Test fun `deleteSend should return a Success with the correct data`() = runTest { server.enqueue(MockResponse().setResponseCode(200)) @@ -96,6 +180,19 @@ class SendsServiceTest : BaseServiceTest() { result.getOrThrow(), ) } + + private fun setupMockUri( + url: String, + queryParams: Map, + ): Uri { + val mockUri = mockk { + queryParams.forEach { + every { getQueryParameter(it.key) } returns it.value + } + } + every { Uri.parse(url) } returns mockUri + return mockUri + } } private const val CREATE_UPDATE_SEND_SUCCESS_JSON = """ @@ -127,6 +224,39 @@ private const val CREATE_UPDATE_SEND_SUCCESS_JSON = """ } """ +private const val CREATE_FILE_SEND_SUCCESS_JSON = """ +{ + "url": "www.test.com", + "fileUploadType": "1", + "sendResponse": { + "id": "mockId-1", + "accessId": "mockAccessId-1", + "type": 1, + "name": "mockName-1", + "notes": "mockNotes-1", + "file": { + "id": "mockId-1", + "fileName": "mockFileName-1", + "size": 1, + "sizeName": "mockSizeName-1" + }, + "text": { + "text": "mockText-1", + "hidden": false + }, + "key": "mockKey-1", + "maxAccessCount": 1, + "accessCount": 1, + "password": "mockPassword-1", + "disabled": false, + "revisionDate": "2023-10-27T12:00:00.00Z", + "expirationDate": "2023-10-27T12:00:00.00Z", + "deletionDate": "2023-10-27T12:00:00.00Z", + "hideEmail": false + } +} +""" + private const val UPDATE_SEND_INVALID_JSON = """ { "message": "You do not have permission to edit this.", diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkSendUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkSendUtil.kt index a7f051e2ce..5171cd35a7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkSendUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkSendUtil.kt @@ -9,7 +9,10 @@ import java.time.ZonedDateTime /** * Create a mock [Send] with a given [number]. */ -fun createMockSdkSend(number: Int): Send = +fun createMockSdkSend( + number: Int, + type: SendType = SendType.FILE, +): Send = Send( id = "mockId-$number", accessId = "mockAccessId-$number", @@ -17,7 +20,7 @@ fun createMockSdkSend(number: Int): Send = notes = "mockNotes-$number", key = "mockKey-$number", password = "mockPassword-$number", - type = SendType.FILE, + type = type, file = createMockSdkFile(number = number), text = createMockSdkText(number = number), maxAccessCount = 1u, 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 ca2b9b97e1..f787a0efca 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 @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.repository +import android.net.Uri import app.cash.turbine.test import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView @@ -7,6 +8,7 @@ import com.bitwarden.core.FolderView import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoRequest +import com.bitwarden.core.SendType import com.bitwarden.core.SendView import com.bitwarden.crypto.Kdf import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson @@ -21,6 +23,8 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.model.SendTypeJson 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.UpdateSendResponseJson @@ -46,6 +50,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCollecti import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView +import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult @@ -68,12 +73,16 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.net.UnknownHostException @@ -81,6 +90,7 @@ import java.net.UnknownHostException class VaultRepositoryTest { private val dispatcherManager: DispatcherManager = FakeDispatcherManager() + private val fileManager: FileManager = mockk() private val fakeAuthDiskSource = FakeAuthDiskSource() private val syncService: SyncService = mockk() private val sendsService: SendsService = mockk() @@ -112,8 +122,19 @@ class VaultRepositoryTest { authDiskSource = fakeAuthDiskSource, vaultLockManager = vaultLockManager, dispatcherManager = dispatcherManager, + fileManager = fileManager, ) + @BeforeEach + fun setup() { + mockkStatic(Uri::class) + } + + @AfterEach + fun tearDown() { + unmockkStatic(Uri::class) + } + @Test fun `ciphersStateFlow should emit decrypted list of ciphers when decryptCipherList succeeds`() = runTest { @@ -1561,51 +1582,181 @@ class VaultRepositoryTest { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) } returns IllegalStateException().asFailure() - val result = vaultRepository.createSend(sendView = mockSendView) + val result = vaultRepository.createSend(sendView = mockSendView, fileUri = null) assertEquals(CreateSendResult.Error, result) } @Test @Suppress("MaxLineLength") - fun `createSend with sendsService createSend failure should return CreateSendResult failure`() = + fun `createSend with TEXT and sendsService createSend failure should return CreateSendResult failure`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "mockId-1" - val mockSendView = createMockSendView(number = 1) + val mockSendView = createMockSendView(number = 1, type = SendType.TEXT) coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns createMockSdkSend(number = 1).asSuccess() + } returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess() coEvery { - sendsService.createSend(body = createMockSendJsonRequest(number = 1)) + sendsService.createSend( + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT) + .copy(fileLength = null), + ) } returns IllegalStateException().asFailure() - val result = vaultRepository.createSend(sendView = mockSendView) + val result = vaultRepository.createSend(sendView = mockSendView, fileUri = null) assertEquals(CreateSendResult.Error, result) } + @Suppress("MaxLineLength") @Test - fun `createSend with sendsService createSend success should return CreateSendResult success`() = + fun `createSend with TEXT and sendsService createSend success should return CreateSendResult success`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "mockId-1" - val mockSendView = createMockSendView(number = 1) - val mockSdkSend = createMockSdkSend(number = 1) - val mockSend = createMockSend(number = 1) + val mockSendView = createMockSendView(number = 1, type = SendType.TEXT) + val mockSdkSend = createMockSdkSend(number = 1, type = SendType.TEXT) + val mockSend = createMockSend(number = 1, type = SendTypeJson.TEXT) val mockSendViewResult = createMockSendView(number = 2) coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) } returns mockSdkSend.asSuccess() coEvery { - sendsService.createSend(body = createMockSendJsonRequest(number = 1)) + sendsService.createSend( + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT) + .copy(fileLength = null), + ) } returns mockSend.asSuccess() coEvery { vaultDiskSource.saveSend(userId, mockSend) } just runs coEvery { vaultSdkSource.decryptSend(userId, mockSdkSend) } returns mockSendViewResult.asSuccess() - val result = vaultRepository.createSend(sendView = mockSendView) + val result = vaultRepository.createSend(sendView = mockSendView, fileUri = null) + + assertEquals(CreateSendResult.Success(mockSendViewResult), result) + } + + @Test + @Suppress("MaxLineLength") + fun `createSend with FILE and sendsService createFileSend failure should return CreateSendResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val uri = setupMockUri(url = "www.test.com") + val mockSendView = createMockSendView(number = 1) + val mockSdkSend = createMockSdkSend(number = 1) + val byteArray = byteArrayOf(1) + val encryptedByteArray = byteArrayOf(2) + coEvery { + vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) + } returns mockSdkSend.asSuccess() + every { fileManager.uriToByteArray(any()) } returns byteArray + coEvery { + vaultSdkSource.encryptBuffer( + userId = userId, + send = mockSdkSend, + fileBuffer = byteArray, + ) + } returns encryptedByteArray.asSuccess() + coEvery { + sendsService.createFileSend(body = createMockSendJsonRequest(number = 1)) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.createSend(sendView = mockSendView, fileUri = uri) + + assertEquals(CreateSendResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createSend with FILE and sendsService uploadFile failure should return CreateSendResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val url = "www.test.com" + val uri = setupMockUri(url = url) + val mockSendView = createMockSendView(number = 1) + val mockSdkSend = createMockSdkSend(number = 1) + val byteArray = byteArrayOf(1) + val encryptedByteArray = byteArrayOf(2) + val sendFileResponse = SendFileResponseJson( + url = url, + fileUploadType = SendFileResponseJson.FileUploadType.AZURE, + sendResponse = createMockSend(number = 1, type = SendTypeJson.FILE), + ) + coEvery { + vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) + } returns mockSdkSend.asSuccess() + every { fileManager.uriToByteArray(any()) } returns byteArray + coEvery { + vaultSdkSource.encryptBuffer( + userId = userId, + send = mockSdkSend, + fileBuffer = byteArray, + ) + } returns encryptedByteArray.asSuccess() + coEvery { + sendsService.createFileSend(body = createMockSendJsonRequest(number = 1)) + } returns sendFileResponse.asSuccess() + coEvery { + sendsService.uploadFile( + sendFileResponse = sendFileResponse, + encryptedFile = encryptedByteArray, + ) + } returns Throwable("Fail").asFailure() + + val result = vaultRepository.createSend(sendView = mockSendView, fileUri = uri) + + assertEquals(CreateSendResult.Error, result) + } + + @Test + @Suppress("MaxLineLength") + fun `createSend with FILE and sendsService uploadFile success should return CreateSendResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val url = "www.test.com" + val uri = setupMockUri(url = url) + val mockSendView = createMockSendView(number = 1) + val mockSdkSend = createMockSdkSend(number = 1) + val byteArray = byteArrayOf(1) + val encryptedByteArray = byteArrayOf(2) + val sendResponse = createMockSend(number = 1) + val sendFileResponse = SendFileResponseJson( + url = url, + fileUploadType = SendFileResponseJson.FileUploadType.AZURE, + sendResponse = sendResponse, + ) + val mockSendViewResult = createMockSendView(number = 1) + coEvery { + vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) + } returns mockSdkSend.asSuccess() + every { fileManager.uriToByteArray(any()) } returns byteArray + coEvery { + vaultSdkSource.encryptBuffer( + userId = userId, + send = mockSdkSend, + fileBuffer = byteArray, + ) + } returns encryptedByteArray.asSuccess() + coEvery { + sendsService.createFileSend(body = createMockSendJsonRequest(number = 1)) + } returns sendFileResponse.asSuccess() + coEvery { + sendsService.uploadFile( + sendFileResponse = sendFileResponse, + encryptedFile = encryptedByteArray, + ) + } returns sendResponse.asSuccess() + coEvery { vaultDiskSource.saveSend(userId, sendResponse) } just runs + coEvery { + vaultSdkSource.decryptSend(userId, mockSdkSend) + } returns mockSendViewResult.asSuccess() + + val result = vaultRepository.createSend(sendView = mockSendView, fileUri = uri) assertEquals(CreateSendResult.Success(mockSendViewResult), result) } @@ -1636,14 +1787,15 @@ class VaultRepositoryTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "mockId-1" val sendId = "sendId1234" - val mockSendView = createMockSendView(number = 1) + val mockSendView = createMockSendView(number = 1, type = SendType.TEXT) coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns createMockSdkSend(number = 1).asSuccess() + } returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess() coEvery { sendsService.updateSend( sendId = sendId, - body = createMockSendJsonRequest(number = 1), + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT) + .copy(fileLength = null), ) } returns IllegalStateException().asFailure() @@ -1662,14 +1814,15 @@ class VaultRepositoryTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "mockId-1" val sendId = "sendId1234" - val mockSendView = createMockSendView(number = 1) + val mockSendView = createMockSendView(number = 1, type = SendType.TEXT) coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns createMockSdkSend(number = 1).asSuccess() + } returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess() coEvery { sendsService.updateSend( sendId = sendId, - body = createMockSendJsonRequest(number = 1), + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT) + .copy(fileLength = null), ) } returns UpdateSendResponseJson .Invalid( @@ -1698,19 +1851,22 @@ class VaultRepositoryTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "mockId-1" val sendId = "sendId1234" - val mockSendView = createMockSendView(number = 1) + val mockSendView = createMockSendView(number = 1, type = SendType.TEXT) coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns createMockSdkSend(number = 1).asSuccess() - val mockSend = createMockSend(number = 1) + } returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess() + val mockSend = createMockSend(number = 1, type = SendTypeJson.TEXT) coEvery { sendsService.updateSend( sendId = sendId, - body = createMockSendJsonRequest(number = 1), + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT) + .copy(fileLength = null), ) } returns UpdateSendResponseJson.Success(send = mockSend).asSuccess() coEvery { - vaultSdkSource.decryptSend(userId = userId, send = createMockSdkSend(number = 1)) + vaultSdkSource.decryptSend( + userId = userId, send = createMockSdkSend(number = 1, type = SendType.TEXT), + ) } returns Throwable("Fail").asFailure() coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs @@ -1729,20 +1885,24 @@ class VaultRepositoryTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "mockId-1" val sendId = "sendId1234" - val mockSendView = createMockSendView(number = 1) + val mockSendView = createMockSendView(number = 1, type = SendType.TEXT) coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns createMockSdkSend(number = 1).asSuccess() - val mockSend = createMockSend(number = 1) + } returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess() + val mockSend = createMockSend(number = 1, type = SendTypeJson.TEXT) coEvery { sendsService.updateSend( sendId = sendId, - body = createMockSendJsonRequest(number = 1), + body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT) + .copy(fileLength = null), ) } returns UpdateSendResponseJson.Success(send = mockSend).asSuccess() - val mockSendViewResult = createMockSendView(number = 2) + val mockSendViewResult = createMockSendView(number = 2, type = SendType.TEXT) coEvery { - vaultSdkSource.decryptSend(userId = userId, send = createMockSdkSend(number = 1)) + vaultSdkSource.decryptSend( + userId = userId, + send = createMockSdkSend(number = 1, type = SendType.TEXT), + ) } returns mockSendViewResult.asSuccess() coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs @@ -1992,6 +2152,19 @@ class VaultRepositoryTest { } } + private fun setupMockUri( + url: String, + queryParams: Map = emptyMap(), + ): Uri { + val mockUri = mockk { + queryParams.forEach { + every { getQueryParameter(it.key) } returns it.value + } + } + every { Uri.parse(url) } returns mockUri + return mockUri + } + //endregion Helper functions } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensionsTest.kt index 58c6f61b10..6d621bc88e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkSendExtensionsTest.kt @@ -8,13 +8,22 @@ import org.junit.jupiter.api.Test class VaultSdkSendExtensionsTest { + @Suppress("MaxLineLength") @Test - fun `toEncryptedNetworkSend should convert a SDK-based Send to network-based Send`() { + fun `toEncryptedNetworkSend should convert a SDK-based Send to network-based Send with file length`() { val sdkSend = createMockSdkSend(number = 1) - val networkSend = sdkSend.toEncryptedNetworkSend() + val networkSend = sdkSend.toEncryptedNetworkSend(fileLength = 1) assertEquals(createMockSendJsonRequest(number = 1), networkSend) } + @Suppress("MaxLineLength") + @Test + fun `toEncryptedNetworkSend should convert a SDK-based Send to network-based Send without file length`() { + val sdkSend = createMockSdkSend(number = 1) + val networkSend = sdkSend.toEncryptedNetworkSend(fileLength = null) + assertEquals(createMockSendJsonRequest(number = 1).copy(fileLength = null), networkSend) + } + @Test fun `toEncryptedSdkSend should convert a network-based Send to SDK-based Send`() { val syncSend = createMockSend(number = 1) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt index ebd656b34d..efcc806c85 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt @@ -120,7 +120,7 @@ class AddSendViewModelTest : BaseViewModelTest() { every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl } coEvery { - vaultRepository.createSend(mockSendView) + vaultRepository.createSend(sendView = mockSendView, fileUri = null) } returns CreateSendResult.Success(sendView = resultSendView) val viewModel = createViewModel(initialState) @@ -131,7 +131,7 @@ class AddSendViewModelTest : BaseViewModelTest() { } assertEquals(initialState, viewModel.stateFlow.value) coVerify(exactly = 1) { - vaultRepository.createSend(mockSendView) + vaultRepository.createSend(sendView = mockSendView, fileUri = null) } } @@ -143,7 +143,9 @@ class AddSendViewModelTest : BaseViewModelTest() { val initialState = DEFAULT_STATE.copy(viewState = viewState) val mockSendView = mockk() every { viewState.toSendView(clock) } returns mockSendView - coEvery { vaultRepository.createSend(mockSendView) } returns CreateSendResult.Error + coEvery { + vaultRepository.createSend(sendView = mockSendView, fileUri = null) + } returns CreateSendResult.Error val viewModel = createViewModel(initialState) viewModel.stateFlow.test { @@ -168,7 +170,7 @@ class AddSendViewModelTest : BaseViewModelTest() { ) } coVerify(exactly = 1) { - vaultRepository.createSend(mockSendView) + vaultRepository.createSend(sendView = mockSendView, fileUri = null) } }