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 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.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest 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.SyncResponseJson
import okhttp3.MultipartBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.POST import retrofit2.http.POST
@ -20,6 +23,25 @@ interface CiphersApi {
@POST("ciphers") @POST("ciphers")
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher> 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. * Updates a cipher.
*/ */

View file

@ -28,9 +28,17 @@ object VaultNetworkModule {
fun provideCiphersService( fun provideCiphersService(
retrofits: Retrofits, retrofits: Retrofits,
json: Json, json: Json,
clock: Clock,
): CiphersService = CiphersServiceImpl( ): CiphersService = CiphersServiceImpl(
azureApi = retrofits
.staticRetrofitBuilder
// This URL will be overridden dynamically
.baseUrl("https://www.bitwarden.com")
.build()
.create(),
ciphersApi = retrofits.authenticatedApiRetrofit.create(), ciphersApi = retrofits.authenticatedApiRetrofit.create(),
json = json, json = json,
clock = clock,
) )
@Provides @Provides
@ -43,7 +51,7 @@ object VaultNetworkModule {
azureApi = retrofits azureApi = retrofits
.staticRetrofitBuilder .staticRetrofitBuilder
// This URL will be overridden dynamically // This URL will be overridden dynamically
.baseUrl("https://www.bitwaredn.com") .baseUrl("https://www.bitwarden.com")
.build() .build()
.create(), .create(),
sendsApi = retrofits.authenticatedApiRetrofit.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 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.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest 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.SyncResponseJson
@ -14,6 +16,22 @@ interface CiphersService {
*/ */
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> 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. * Attempt to update a cipher.
*/ */

View file

@ -1,21 +1,88 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service 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.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull 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.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.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.ShareCipherJsonRequest
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.UpdateCipherResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import kotlinx.serialization.json.Json 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 ciphersApi: CiphersApi,
private val json: Json, private val json: Json,
private val clock: Clock,
) : CiphersService { ) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> = override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
ciphersApi.createCipher(body = body) 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( override suspend fun updateCipher(
cipherId: String, cipherId: String,
body: CipherJsonRequest, 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.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem 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.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@ -182,6 +183,17 @@ interface VaultRepository : VaultLockManager {
*/ */
suspend fun createCipher(cipherView: CipherView): CreateCipherResult 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. * Attempt to delete a cipher.
*/ */

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.repository package com.x8bit.bitwarden.data.vault.repository
import android.net.Uri import android.net.Uri
import com.bitwarden.core.AttachmentView
import com.bitwarden.core.CipherType import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView 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.asFailure
import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource 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.ShareCipherJsonRequest
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.UpdateCipherResponseJson 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.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem 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.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult import com.x8bit.bitwarden.data.vault.repository.model.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.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse 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.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.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList 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( override suspend fun createSend(
sendView: SendView, sendView: SendView,
fileUri: Uri?, 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 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.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.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.ShareCipherJsonRequest
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.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.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest 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 kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
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.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class CiphersServiceTest : BaseServiceTest() { 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 ciphersApi: CiphersApi = retrofit.create()
private val ciphersService: CiphersService = CiphersServiceImpl( private val ciphersService: CiphersService = CiphersServiceImpl(
azureApi = azureApi,
ciphersApi = ciphersApi, ciphersApi = ciphersApi,
json = json, json = json,
clock = clock,
) )
@BeforeEach
fun setup() {
mockkStatic(Uri::class)
}
@AfterEach
fun tearDown() {
unmockkStatic(Uri::class)
}
@Test @Test
fun `createCipher should return the correct response`() = runTest { fun `createCipher should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_UPDATE_CIPHER_SUCCESS_JSON)) 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 @Test
fun `updateCipher with success response should return a Success with the correct cipher`() = fun `updateCipher with success response should return a Success with the correct cipher`() =
runTest { 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 = """ private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """
{ {
"notes": "mockNotes-1", "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.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource 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.FileUploadType
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson 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.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.SyncResponseJson
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.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.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
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection 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.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource 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.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.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
@ -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.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem 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.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult import com.x8bit.bitwarden.data.vault.repository.model.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.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult 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.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.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList 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 @Test
fun `generateTotp should return a success result on getting a code`() = runTest { fun `generateTotp should return a success result on getting a code`() = runTest {
val totpResponse = TotpResponse("Testcode", 30u) val totpResponse = TotpResponse("Testcode", 30u)
@ -2728,12 +3060,6 @@ class VaultRepositoryTest {
return mockUri return mockUri
} }
private fun setupMockInstant(): Instant {
val mockInstant = mockk<Instant>()
every { Instant.now() } returns Instant.MIN
return mockInstant
}
//endregion Helper functions //endregion Helper functions
} }