Add flow for creating attachments (#777)

This commit is contained in:
David Perez 2024-01-25 11:46:37 -06:00 committed by Álison Fernandes
parent 3e9852e9e7
commit 465cce42f0
14 changed files with 822 additions and 8 deletions

View file

@ -1,8 +1,11 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
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
@ -20,6 +23,25 @@ interface CiphersApi {
@POST("ciphers")
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Associates an attachment with a cipher.
*/
@POST("ciphers/{cipherId}/attachment/v2")
suspend fun createAttachment(
@Path("cipherId") cipherId: String,
@Body body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse>
/**
* Uploads the attachment associated with a cipher.
*/
@POST("ciphers/{cipherId}/attachment/{attachmentId}")
suspend fun uploadAttachment(
@Path("cipherId") cipherId: String,
@Path("attachmentId") attachmentId: String,
@Body body: MultipartBody,
): Result<Unit>
/**
* Updates a cipher.
*/

View file

@ -28,9 +28,17 @@ object VaultNetworkModule {
fun provideCiphersService(
retrofits: Retrofits,
json: Json,
clock: Clock,
): CiphersService = CiphersServiceImpl(
azureApi = retrofits
.staticRetrofitBuilder
// This URL will be overridden dynamically
.baseUrl("https://www.bitwarden.com")
.build()
.create(),
ciphersApi = retrofits.authenticatedApiRetrofit.create(),
json = json,
clock = clock,
)
@Provides
@ -43,7 +51,7 @@ object VaultNetworkModule {
azureApi = retrofits
.staticRetrofitBuilder
// This URL will be overridden dynamically
.baseUrl("https://www.bitwaredn.com")
.baseUrl("https://www.bitwarden.com")
.build()
.create(),
sendsApi = retrofits.authenticatedApiRetrofit.create(),

View file

@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a request to create an attachment.
*/
@Serializable
data class AttachmentJsonRequest(
@SerialName("fileName")
val fileName: String,
@SerialName("key")
val key: String,
@SerialName("fileSize")
val fileSize: String,
)

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents the JSON response from creating a new attachment.
*/
@Serializable
data class AttachmentJsonResponse(
@SerialName("attachmentId")
val attachmentId: String,
@SerialName("url")
val url: String,
@SerialName("fileUploadType")
val fileUploadType: FileUploadType,
@SerialName("cipherResponse")
val cipherResponse: SyncResponseJson.Cipher,
)

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@ -14,6 +16,22 @@ interface CiphersService {
*/
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Attempt to upload an attachment file.
*/
suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse,
encryptedFile: ByteArray,
): Result<SyncResponseJson.Cipher>
/**
* Attempt to create an attachment.
*/
suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse>
/**
* Attempt to update a cipher.
*/

View file

@ -1,21 +1,88 @@
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.CiphersApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import 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
class CiphersServiceImpl constructor(
class CiphersServiceImpl(
private val azureApi: AzureApi,
private val ciphersApi: CiphersApi,
private val json: Json,
private val clock: Clock,
) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
ciphersApi.createCipher(body = body)
override suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse> =
ciphersApi.createAttachment(
cipherId = cipherId,
body = body,
)
override suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse,
encryptedFile: ByteArray,
): Result<SyncResponseJson.Cipher> {
val cipher = attachmentJsonResponse.cipherResponse
return when (attachmentJsonResponse.fileUploadType) {
FileUploadType.DIRECT -> {
ciphersApi.uploadAttachment(
cipherId = requireNotNull(cipher.id),
attachmentId = attachmentJsonResponse.attachmentId,
body = MultipartBody
.Builder(
boundary = "--BWMobileFormBoundary${clock.instant().toEpochMilli()}",
)
.addPart(
part = MultipartBody.Part.createFormData(
body = encryptedFile.toRequestBody(
contentType = "application/octet-stream".toMediaType(),
),
name = "data",
filename = cipher
.attachments
?.find { it.id == attachmentJsonResponse.attachmentId }
?.fileName,
),
)
.build(),
)
}
FileUploadType.AZURE -> {
azureApi.uploadAzureBlob(
url = attachmentJsonResponse.url,
date = DateTimeFormatter
.RFC_1123_DATE_TIME
.format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)),
version = attachmentJsonResponse.url.toUri().getQueryParameter("sv"),
body = encryptedFile.toRequestBody(),
)
}
}
.map { cipher }
}
override suspend fun updateCipher(
cipherId: String,
body: CipherJsonRequest,

View file

@ -11,6 +11,7 @@ import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@ -182,6 +183,17 @@ interface VaultRepository : VaultLockManager {
*/
suspend fun createCipher(cipherView: CipherView): CreateCipherResult
/**
* Attempt to create an attachment for the given [cipherView].
*/
suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult
/**
* Attempt to delete a cipher.
*/

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.repository
import android.net.Uri
import com.bitwarden.core.AttachmentView
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
@ -26,6 +27,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoadin
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.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
@ -38,6 +40,7 @@ import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@ -56,6 +59,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
@ -643,6 +647,71 @@ class VaultRepositoryImpl(
)
}
override suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult {
val userId = requireNotNull(activeUserId)
val attachmentView = AttachmentView(
id = null,
url = null,
size = fileSizeBytes,
sizeName = null,
fileName = fileName,
key = null,
)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = cipher,
attachmentView = attachmentView,
fileBuffer = fileManager.uriToByteArray(fileUri = fileUri),
)
}
.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,
)
}
}
.onSuccess {
// Save the send immediately, regardless of whether the decrypt succeeds
vaultDiskSource.saveCipher(userId = userId, cipher = it)
}
.flatMap {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = it.toEncryptedSdkCipher(),
)
}
.fold(
onFailure = { CreateAttachmentResult.Error },
onSuccess = { CreateAttachmentResult.Success(it) },
)
}
override suspend fun createSend(
sendView: SendView,
fileUri: Uri?,

View file

@ -0,0 +1,21 @@
package com.x8bit.bitwarden.data.vault.repository.model
import com.bitwarden.core.CipherView
/**
* Models result of creating an attachment.
*/
sealed class CreateAttachmentResult {
/**
* Attachment created successfully.
*/
data class Success(
val cipherView: CipherView,
) : CreateAttachmentResult()
/**
* Generic error while creating an attachment.
*/
data object Error : CreateAttachmentResult()
}

View file

@ -0,0 +1,13 @@
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

@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
/**
* Create a mock [CipherJsonRequest] with a given [number].
*/
fun createMockAttachmentJsonRequest(number: Int): AttachmentJsonRequest =
AttachmentJsonRequest(
fileName = "mockFileName-$number",
key = "mockKey-$number",
fileSize = "1000",
)

View file

@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
/**
* Create a mock [AttachmentJsonResponse] with a given [number].
*/
fun createMockAttachmentJsonResponse(
number: Int,
fileUploadType: FileUploadType = FileUploadType.AZURE,
): AttachmentJsonResponse =
AttachmentJsonResponse(
attachmentId = "mockAttachmentId-$number",
url = "mockUrl-$number",
fileUploadType = fileUploadType,
cipherResponse = createMockCipher(number = number),
)

View file

@ -1,25 +1,56 @@
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.CiphersApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest
import 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 CiphersServiceTest : BaseServiceTest() {
private val clock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val azureApi: AzureApi = retrofit.create()
private val ciphersApi: CiphersApi = retrofit.create()
private val ciphersService: CiphersService = CiphersServiceImpl(
azureApi = azureApi,
ciphersApi = ciphersApi,
json = json,
clock = clock,
)
@BeforeEach
fun setup() {
mockkStatic(Uri::class)
}
@AfterEach
fun tearDown() {
unmockkStatic(Uri::class)
}
@Test
fun `createCipher should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_UPDATE_CIPHER_SUCCESS_JSON))
@ -32,6 +63,56 @@ class CiphersServiceTest : BaseServiceTest() {
)
}
@Test
fun `createAttachment should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_ATTACHMENT_SUCCESS_JSON))
val result = ciphersService.createAttachment(
cipherId = "mockId-1",
body = createMockAttachmentJsonRequest(number = 1),
)
assertEquals(
createMockAttachmentJsonResponse(number = 1),
result.getOrThrow(),
)
}
@Test
fun `uploadAttachment with Azure uploadFile success should return cipher`() = runTest {
setupMockUri(url = "mockUrl-1", queryParams = mapOf("sv" to "2024-04-03"))
val mockCipher = createMockCipher(number = 1)
val attachmentJsonResponse = createMockAttachmentJsonResponse(
number = 1,
fileUploadType = FileUploadType.AZURE,
)
val encryptedFile = byteArrayOf()
server.enqueue(MockResponse().setResponseCode(201))
val result = ciphersService.uploadAttachment(
attachmentJsonResponse = attachmentJsonResponse,
encryptedFile = encryptedFile,
)
assertEquals(mockCipher, result.getOrThrow())
}
@Test
fun `uploadAttachment with Direct uploadFile success should return cipher`() = runTest {
val mockCipher = createMockCipher(number = 1)
val attachmentJsonResponse = createMockAttachmentJsonResponse(
number = 1,
fileUploadType = FileUploadType.DIRECT,
)
val encryptedFile = byteArrayOf()
server.enqueue(MockResponse().setResponseCode(201))
val result = ciphersService.uploadAttachment(
attachmentJsonResponse = attachmentJsonResponse,
encryptedFile = encryptedFile,
)
assertEquals(mockCipher, result.getOrThrow())
}
@Test
fun `updateCipher with success response should return a Success with the correct cipher`() =
runTest {
@ -123,6 +204,116 @@ class CiphersServiceTest : BaseServiceTest() {
}
}
private fun setupMockUri(
url: String,
queryParams: Map<String, String>,
): Uri {
val mockUri = mockk<Uri> {
queryParams.forEach {
every { getQueryParameter(it.key) } returns it.value
}
}
every { Uri.parse(url) } returns mockUri
return mockUri
}
private const val CREATE_ATTACHMENT_SUCCESS_JSON = """
{
"attachmentId":"mockAttachmentId-1",
"url":"mockUrl-1",
"fileUploadType":1,
"cipherResponse":{
"notes": "mockNotes-1",
"attachments": [
{
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1",
"url": "mockUrl-1",
"key": "mockKey-1"
}
],
"organizationUseTotp": false,
"reprompt": 0,
"edit": false,
"passwordHistory": [
{
"password": "mockPassword-1",
"lastUsedDate": "2023-10-27T12:00:00.00Z"
}
],
"revisionDate": "2023-10-27T12:00:00.00Z",
"type": 1,
"login": {
"uris": [
{
"match": 1,
"uri": "mockUri-1"
}
],
"totp": "mockTotp-1",
"password": "mockPassword-1",
"passwordRevisionDate": "2023-10-27T12:00:00.00Z",
"autofillOnPageLoad": false,
"uri": "mockUri-1",
"username": "mockUsername-1"
},
"creationDate": "2023-10-27T12:00:00.00Z",
"secureNote": {
"type": 0
},
"folderId": "mockFolderId-1",
"organizationId": "mockOrganizationId-1",
"deletedDate": "2023-10-27T12:00:00.00Z",
"identity": {
"passportNumber": "mockPassportNumber-1",
"lastName": "mockLastName-1",
"address3": "mockAddress3-1",
"address2": "mockAddress2-1",
"city": "mockCity-1",
"country": "mockCountry-1",
"address1": "mockAddress1-1",
"postalCode": "mockPostalCode-1",
"title": "mockTitle-1",
"ssn": "mockSsn-1",
"firstName": "mockFirstName-1",
"phone": "mockPhone-1",
"middleName": "mockMiddleName-1",
"company": "mockCompany-1",
"licenseNumber": "mockLicenseNumber-1",
"state": "mockState-1",
"email": "mockEmail-1",
"username": "mockUsername-1"
},
"collectionIds": [
"mockCollectionId-1"
],
"name": "mockName-1",
"id": "mockId-1"
"fields": [
{
"linkedId": 100,
"name": "mockName-1",
"type": 1,
"value": "mockValue-1"
}
],
"viewPassword": false,
"favorite": false,
"card": {
"number": "mockNumber-1",
"expMonth": "mockExpMonth-1",
"code": "mockCode-1",
"expYear": "mockExpirationYear-1",
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
"key": "mockKey-1"
}
}
"""
private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """
{
"notes": "mockNotes-1",

View file

@ -27,6 +27,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendTypeJson
@ -34,6 +35,8 @@ 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.UpdateCipherResponseJson
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
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection
@ -48,6 +51,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAttachmentView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
@ -60,6 +64,7 @@ import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@ -76,6 +81,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
@ -2367,6 +2373,332 @@ class VaultRepositoryTest {
)
}
@Suppress("MaxLineLength")
@Test
fun `createAttachment with encryptCipher failure should return CreateAttachmentResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "cipherId-1"
val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1)
val mockFileName = "mockFileName-1"
val mockFileSize = "1"
coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.createAttachment(
cipherId = cipherId,
cipherView = mockCipherView,
fileSizeBytes = mockFileSize,
fileName = mockFileName,
fileUri = mockUri,
)
assertEquals(CreateAttachmentResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `createAttachment with encryptAttachment failure should return CreateAttachmentResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "cipherId-1"
val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1)
val mockCipher = createMockSdkCipher(number = 1)
val mockFileName = "mockFileName-1"
val mockFileSize = "1"
val mockAttachmentView = createMockAttachmentView(number = 1).copy(
sizeName = null,
id = null,
url = null,
key = null,
)
val mockByteArray = byteArrayOf(1, 2)
coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess()
every { fileManager.uriToByteArray(fileUri = mockUri) } returns mockByteArray
coEvery {
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = mockCipher,
attachmentView = mockAttachmentView,
fileBuffer = mockByteArray,
)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.createAttachment(
cipherId = cipherId,
cipherView = mockCipherView,
fileSizeBytes = mockFileSize,
fileName = mockFileName,
fileUri = mockUri,
)
assertEquals(CreateAttachmentResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `createAttachment with createAttachment failure should return CreateAttachmentResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "cipherId-1"
val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1)
val mockCipher = createMockSdkCipher(number = 1)
val mockFileName = "mockFileName-1"
val mockFileSize = "1"
val mockAttachmentView = createMockAttachmentView(number = 1).copy(
sizeName = null,
id = null,
url = null,
key = null,
)
val mockByteArray = byteArrayOf(1, 2)
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1)
coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess()
every { fileManager.uriToByteArray(fileUri = mockUri) } returns mockByteArray
coEvery {
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = mockCipher,
attachmentView = mockAttachmentView,
fileBuffer = mockByteArray,
)
} returns mockAttachmentEncryptResult.asSuccess()
coEvery {
ciphersService.createAttachment(
cipherId = cipherId,
body = AttachmentJsonRequest(
fileName = mockFileName,
key = "mockKey-1",
fileSize = mockFileSize,
),
)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.createAttachment(
cipherId = cipherId,
cipherView = mockCipherView,
fileSizeBytes = mockFileSize,
fileName = mockFileName,
fileUri = mockUri,
)
assertEquals(CreateAttachmentResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `createAttachment with uploadAttachment failure should return CreateAttachmentResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "cipherId-1"
val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1)
val mockCipher = createMockSdkCipher(number = 1)
val mockFileName = "mockFileName-1"
val mockFileSize = "1"
val mockAttachmentView = createMockAttachmentView(number = 1).copy(
sizeName = null,
id = null,
url = null,
key = null,
)
val mockByteArray = byteArrayOf(1, 2)
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1)
val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1)
coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess()
every { fileManager.uriToByteArray(fileUri = mockUri) } returns mockByteArray
coEvery {
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = mockCipher,
attachmentView = mockAttachmentView,
fileBuffer = mockByteArray,
)
} returns mockAttachmentEncryptResult.asSuccess()
coEvery {
ciphersService.createAttachment(
cipherId = cipherId,
body = AttachmentJsonRequest(
fileName = mockFileName,
key = "mockKey-1",
fileSize = mockFileSize,
),
)
} returns mockAttachmentJsonResponse.asSuccess()
coEvery {
ciphersService.uploadAttachment(
attachmentJsonResponse = mockAttachmentJsonResponse,
encryptedFile = mockAttachmentEncryptResult.contents,
)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.createAttachment(
cipherId = cipherId,
cipherView = mockCipherView,
fileSizeBytes = mockFileSize,
fileName = mockFileName,
fileUri = mockUri,
)
assertEquals(CreateAttachmentResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `createAttachment with decryptCipher failure should return CreateAttachmentResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "cipherId-1"
val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1)
val mockCipher = createMockSdkCipher(number = 1)
val mockFileName = "mockFileName-1"
val mockFileSize = "1"
val mockAttachmentView = createMockAttachmentView(number = 1).copy(
sizeName = null,
id = null,
url = null,
key = null,
)
val mockByteArray = byteArrayOf(1, 2)
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1)
val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1)
val mockCipherResponse = createMockCipher(number = 1)
coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess()
every { fileManager.uriToByteArray(fileUri = mockUri) } returns mockByteArray
coEvery {
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = mockCipher,
attachmentView = mockAttachmentView,
fileBuffer = mockByteArray,
)
} returns mockAttachmentEncryptResult.asSuccess()
coEvery {
ciphersService.createAttachment(
cipherId = cipherId,
body = AttachmentJsonRequest(
fileName = mockFileName,
key = "mockKey-1",
fileSize = mockFileSize,
),
)
} returns mockAttachmentJsonResponse.asSuccess()
coEvery {
ciphersService.uploadAttachment(
attachmentJsonResponse = mockAttachmentJsonResponse,
encryptedFile = mockAttachmentEncryptResult.contents,
)
} returns mockCipherResponse.asSuccess()
coEvery {
vaultDiskSource.saveCipher(userId = userId, cipher = mockCipherResponse)
} just runs
coEvery {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = mockCipherResponse.toEncryptedSdkCipher(),
)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.createAttachment(
cipherId = cipherId,
cipherView = mockCipherView,
fileSizeBytes = mockFileSize,
fileName = mockFileName,
fileUri = mockUri,
)
assertEquals(CreateAttachmentResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `createAttachment with createAttachment success should return CreateAttachmentResult Success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "cipherId-1"
val mockUri = setupMockUri(url = "www.test.com")
val mockCipherView = createMockCipherView(number = 1)
val mockCipher = createMockSdkCipher(number = 1)
val mockFileName = "mockFileName-1"
val mockFileSize = "1"
val mockAttachmentView = createMockAttachmentView(number = 1).copy(
sizeName = null,
id = null,
url = null,
key = null,
)
val mockByteArray = byteArrayOf(1, 2)
val mockAttachmentEncryptResult = createMockAttachmentEncryptResult(number = 1)
val mockAttachmentJsonResponse = createMockAttachmentJsonResponse(number = 1)
val mockCipherResponse = createMockCipher(number = 1)
coEvery {
vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView)
} returns mockCipher.asSuccess()
every { fileManager.uriToByteArray(fileUri = mockUri) } returns mockByteArray
coEvery {
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = mockCipher,
attachmentView = mockAttachmentView,
fileBuffer = mockByteArray,
)
} returns mockAttachmentEncryptResult.asSuccess()
coEvery {
ciphersService.createAttachment(
cipherId = cipherId,
body = AttachmentJsonRequest(
fileName = mockFileName,
key = "mockKey-1",
fileSize = mockFileSize,
),
)
} returns mockAttachmentJsonResponse.asSuccess()
coEvery {
ciphersService.uploadAttachment(
attachmentJsonResponse = mockAttachmentJsonResponse,
encryptedFile = mockAttachmentEncryptResult.contents,
)
} returns mockCipherResponse.asSuccess()
coEvery {
vaultDiskSource.saveCipher(userId = userId, cipher = mockCipherResponse)
} just runs
coEvery {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = mockCipherResponse.toEncryptedSdkCipher(),
)
} returns mockCipherView.asSuccess()
val result = vaultRepository.createAttachment(
cipherId = cipherId,
cipherView = mockCipherView,
fileSizeBytes = mockFileSize,
fileName = mockFileName,
fileUri = mockUri,
)
assertEquals(CreateAttachmentResult.Success(mockCipherView), result)
}
@Test
fun `generateTotp should return a success result on getting a code`() = runTest {
val totpResponse = TotpResponse("Testcode", 30u)
@ -2728,12 +3060,6 @@ class VaultRepositoryTest {
return mockUri
}
private fun setupMockInstant(): Instant {
val mockInstant = mockk<Instant>()
every { Instant.now() } returns Instant.MIN
return mockInstant
}
//endregion Helper functions
}