Add API support for deleting an attachment (#767)

This commit is contained in:
David Perez 2024-01-24 18:07:13 -06:00 committed by Álison Fernandes
parent 049930da40
commit 6223b225c5
8 changed files with 172 additions and 0 deletions

View file

@ -54,6 +54,15 @@ interface CiphersApi {
@Path("cipherId") cipherId: String,
): Result<Unit>
/**
* Deletes an attachment from a cipher.
*/
@DELETE("ciphers/{cipherId}/attachment/{attachmentId}")
suspend fun deleteCipherAttachment(
@Path("cipherId") cipherId: String,
@Path("attachmentId") attachmentId: String,
): Result<Unit>
/**
* Restores a cipher.
*/

View file

@ -40,6 +40,14 @@ interface CiphersService {
*/
suspend fun softDeleteCipher(cipherId: String): Result<Unit>
/**
* Attempt to delete an attachment from a cipher.
*/
suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
): Result<Unit>
/**
* Attempt to restore a cipher.
*/

View file

@ -51,6 +51,15 @@ class CiphersServiceImpl constructor(
override suspend fun softDeleteCipher(cipherId: String): Result<Unit> =
ciphersApi.softDeleteCipher(cipherId = cipherId)
override suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
): Result<Unit> =
ciphersApi.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
override suspend fun restoreCipher(cipherId: String): Result<Unit> =
ciphersApi.restoreCipher(cipherId = cipherId)
}

View file

@ -13,6 +13,7 @@ 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.CreateCipherResult
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.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@ -241,4 +242,13 @@ interface VaultRepository : VaultLockManager {
* Attempt to delete a send.
*/
suspend fun deleteSend(sendId: String): DeleteSendResult
/**
* Attempt to delete an attachment from a send.
*/
suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
cipherView: CipherView,
): DeleteAttachmentResult
}

View file

@ -40,6 +40,7 @@ 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.CreateCipherResult
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.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@ -487,6 +488,40 @@ class VaultRepositoryImpl(
)
}
override suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
cipherView: CipherView,
): DeleteAttachmentResult {
val userId = requireNotNull(activeUserId)
return ciphersService
.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
.flatMap {
vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView.copy(
attachments = cipherView.attachments?.mapNotNull {
if (it.id == attachmentId) null else it
},
),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { DeleteAttachmentResult.Success },
onFailure = { DeleteAttachmentResult.Error },
)
}
override suspend fun restoreCipher(
cipherId: String,
cipherView: CipherView,

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models result of deleting an attachment from a cipher.
*/
sealed class DeleteAttachmentResult {
/**
* Attachment deleted successfully.
*/
data object Success : DeleteAttachmentResult()
/**
* Generic error while deleting an attachment.
*/
data object Error : DeleteAttachmentResult()
}

View file

@ -81,6 +81,18 @@ class CiphersServiceTest : BaseServiceTest() {
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `deleteCipherAttachment should execute the deleteCipherAttachment API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val cipherId = "cipherId"
val attachmentId = "attachmentId"
val result = ciphersService.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `shareCipher should execute the share cipher API`() = runTest {
server.enqueue(

View file

@ -61,6 +61,7 @@ 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.CreateCipherResult
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.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@ -1703,6 +1704,77 @@ class VaultRepositoryTest {
unmockkStatic(Cipher::toEncryptedNetworkCipherResponse)
}
@Suppress("MaxLineLength")
@Test
fun `deleteCipherAttachment with ciphersService deleteCipherAttachment failure should return DeleteAttachmentResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val cipherId = "mockId-1"
val attachmentId = "mockId-1"
coEvery {
ciphersService.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
cipherView = createMockCipherView(number = 1),
)
assertEquals(DeleteAttachmentResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `deleteCipherAttachment with ciphersService deleteCipherAttachment success should return DeleteAttachmentResult success`() =
runTest {
mockkStatic(Cipher::toEncryptedNetworkCipherResponse)
every {
createMockSdkCipher(number = 1).toEncryptedNetworkCipherResponse()
} returns createMockCipher(number = 1)
val fixedInstant = Instant.parse("2021-01-01T00:00:00Z")
val userId = "mockId-1"
val cipherId = "mockId-1"
val attachmentId = "mockId-1"
coEvery {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = createMockCipherView(number = 1).copy(
attachments = emptyList(),
),
)
} returns createMockSdkCipher(number = 1).asSuccess()
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
ciphersService.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
} returns Unit.asSuccess()
coEvery {
vaultDiskSource.saveCipher(
userId = userId,
cipher = createMockCipher(number = 1),
)
} returns Unit
val cipherView = createMockCipherView(number = 1)
mockkStatic(Instant::class)
every { Instant.now() } returns fixedInstant
val result = vaultRepository.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
cipherView = cipherView,
)
assertEquals(DeleteAttachmentResult.Success, result)
unmockkStatic(Instant::class)
unmockkStatic(Cipher::toEncryptedNetworkCipherResponse)
}
@Suppress("MaxLineLength")
@Test
fun `restoreCipher with ciphersService restoreCipher failure should return RestoreCipherResult Error`() =