Add API for updating a cipher (#354)

This commit is contained in:
David Perez 2023-12-08 10:23:11 -06:00 committed by Álison Fernandes
parent 266db5cc04
commit bfc0e9831c
8 changed files with 168 additions and 2 deletions

View file

@ -4,6 +4,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
/**
* Defines raw calls under the /ciphers API with authentication applied.
@ -15,4 +17,13 @@ interface CiphersApi {
*/
@POST("ciphers")
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Updates a cipher.
*/
@PUT("ciphers/{cipherId}")
suspend fun updateCipher(
@Path("cipherId") cipherId: String,
@Body body: CipherJsonRequest,
): Result<SyncResponseJson.Cipher>
}

View file

@ -11,4 +11,12 @@ interface CiphersService {
* Attempt to create a cipher.
*/
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Attempt to update a cipher.
*/
suspend fun updateCipher(
cipherId: String,
body: CipherJsonRequest,
): Result<SyncResponseJson.Cipher>
}

View file

@ -9,4 +9,13 @@ class CiphersServiceImpl constructor(
) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
ciphersApi.createCipher(body = body)
override suspend fun updateCipher(
cipherId: String,
body: CipherJsonRequest,
): Result<SyncResponseJson.Cipher> =
ciphersApi.updateCipher(
cipherId = cipherId,
body = body,
)
}

View file

@ -6,6 +6,7 @@ import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
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
@ -86,4 +87,12 @@ interface VaultRepository {
* Attempt to create a cipher.
*/
suspend fun createCipher(cipherView: CipherView): CreateCipherResult
/**
* Attempt to update a cipher.
*/
suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult
}

View file

@ -19,6 +19,7 @@ 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.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
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
@ -256,6 +257,26 @@ class VaultRepositoryImpl constructor(
},
)
override suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult =
vaultSdkSource
.encryptCipher(cipherView = cipherView)
.flatMap { cipher ->
ciphersService.updateCipher(
cipherId = cipherId,
body = cipher.toEncryptedNetworkCipher(),
)
}
.fold(
onFailure = { UpdateCipherResult.Error },
onSuccess = {
sync()
UpdateCipherResult.Success
},
)
// TODO: This is temporary. Eventually this needs to be based on the presence of various
// user keys but this will likely require SDK updates to support this (BIT-1190).
private fun setVaultToUnlocked(userId: String) {

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models result of updating a cipher.
*/
sealed class UpdateCipherResult {
/**
* Cipher updated successfully.
*/
data object Success : UpdateCipherResult()
/**
* Generic error while updating cipher.
*/
data object Error : UpdateCipherResult()
}

View file

@ -19,7 +19,7 @@ class CiphersServiceTest : BaseServiceTest() {
@Test
fun `createCipher should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_CIPHER_SUCCESS_JSON))
server.enqueue(MockResponse().setBody(CREATE_UPDATE_CIPHER_SUCCESS_JSON))
val result = ciphersService.createCipher(
body = createMockCipherJsonRequest(number = 1),
)
@ -28,9 +28,22 @@ class CiphersServiceTest : BaseServiceTest() {
result.getOrThrow(),
)
}
@Test
fun `updateCipher should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_UPDATE_CIPHER_SUCCESS_JSON))
val result = ciphersService.updateCipher(
cipherId = "cipher-id-1",
body = createMockCipherJsonRequest(number = 1),
)
assertEquals(
createMockCipher(number = 1),
result.getOrThrow(),
)
}
}
private const val CREATE_CIPHER_SUCCESS_JSON = """
private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """
{
"notes": "mockNotes-1",
"attachments": [

View file

@ -31,6 +31,7 @@ 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.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
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
@ -1484,6 +1485,83 @@ class VaultRepositoryTest {
)
}
@Test
fun `updateCipher with encryptCipher failure should return UpdateCipherResult failure`() =
runTest {
val cipherId = "cipherId1234"
val mockCipherView = createMockCipherView(number = 1)
coEvery {
vaultSdkSource.encryptCipher(cipherView = mockCipherView)
} returns IllegalStateException().asFailure()
val result = vaultRepository.updateCipher(
cipherId = cipherId,
cipherView = mockCipherView,
)
assertEquals(UpdateCipherResult.Error, result)
}
@Test
@Suppress("MaxLineLength")
fun `updateCipher with ciphersService updateCipher failure should return UpdateCipherResult failure`() =
runTest {
val cipherId = "cipherId1234"
val mockCipherView = createMockCipherView(number = 1)
coEvery {
vaultSdkSource.encryptCipher(cipherView = mockCipherView)
} returns createMockSdkCipher(number = 1).asSuccess()
coEvery {
ciphersService.updateCipher(
cipherId = cipherId,
body = createMockCipherJsonRequest(number = 1),
)
} returns IllegalStateException().asFailure()
val result = vaultRepository.updateCipher(
cipherId = cipherId,
cipherView = mockCipherView,
)
assertEquals(UpdateCipherResult.Error, result)
}
@Test
@Suppress("MaxLineLength")
fun `updateCipher with ciphersService updateCipher success should return UpdateCipherResult success`() =
runTest {
val cipherId = "cipherId1234"
val mockCipherView = createMockCipherView(number = 1)
coEvery {
vaultSdkSource.encryptCipher(cipherView = mockCipherView)
} returns createMockSdkCipher(number = 1).asSuccess()
coEvery {
ciphersService.updateCipher(
cipherId = cipherId,
body = createMockCipherJsonRequest(number = 1),
)
} returns createMockCipher(number = 1).asSuccess()
coEvery {
syncService.sync()
} returns Result.success(createMockSyncResponse(1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherView(1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(1)).asSuccess()
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1)))
} returns listOf(createMockSendView(1)).asSuccess()
val result = vaultRepository.updateCipher(
cipherId = cipherId,
cipherView = mockCipherView,
)
assertEquals(UpdateCipherResult.Success, result)
}
//region Helper functions
/**