Add the underlying support for deleting a Cipher (#587)

This commit is contained in:
David Perez 2024-01-11 22:37:24 -06:00 committed by Álison Fernandes
parent b29af03b27
commit 1f0a1bba6f
13 changed files with 129 additions and 1 deletions

View file

@ -19,6 +19,11 @@ interface VaultDiskSource {
*/
fun getCiphers(userId: String): Flow<List<SyncResponseJson.Cipher>>
/**
* Deletes a cipher from the data source for the given [userId] and [cipherId].
*/
suspend fun deleteCipher(userId: String, cipherId: String)
/**
* Saves a collection to the data source for the given [userId].
*/

View file

@ -64,6 +64,10 @@ class VaultDiskSourceImpl(
},
)
override suspend fun deleteCipher(userId: String, cipherId: String) {
ciphersDao.deleteCipher(userId, cipherId)
}
override suspend fun saveCollection(userId: String, collection: SyncResponseJson.Collection) {
collectionsDao.insertCollection(
collection = CollectionEntity(

View file

@ -36,6 +36,13 @@ interface CiphersDao {
@Query("DELETE FROM ciphers WHERE user_id = :userId")
suspend fun deleteAllCiphers(userId: String): Int
/**
* Deletes the specified cipher associated with the given [userId] and [cipherId]. This will
* return the number of rows deleted by this query.
*/
@Query("DELETE FROM sends WHERE user_id = :userId AND id = :cipherId")
suspend fun deleteCipher(userId: String, cipherId: String): Int
/**
* Deletes all the stored ciphers associated with the given [userId] and then add all new
* [ciphers] to the database. This will return `true` if any changes were made to the database

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api
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.DELETE
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
@ -26,4 +27,12 @@ interface CiphersApi {
@Path("cipherId") cipherId: String,
@Body body: CipherJsonRequest,
): Result<SyncResponseJson.Cipher>
/**
* Deletes a cipher.
*/
@DELETE("ciphers/{cipherId}")
suspend fun deleteCipher(
@Path("cipherId") cipherId: String,
): Result<Unit>
}

View file

@ -20,4 +20,9 @@ interface CiphersService {
cipherId: String,
body: CipherJsonRequest,
): Result<UpdateCipherResponseJson>
/**
* Attempt to delete a cipher.
*/
suspend fun deleteCipher(cipherId: String): Result<Unit>
}

View file

@ -34,4 +34,7 @@ class CiphersServiceImpl constructor(
)
?: throw throwable
}
override suspend fun deleteCipher(cipherId: String): Result<Unit> =
ciphersApi.deleteCipher(cipherId = cipherId)
}

View file

@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
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
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.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
@ -133,6 +134,11 @@ interface VaultRepository : VaultLockManager {
*/
suspend fun createCipher(cipherView: CipherView): CreateCipherResult
/**
* Attempt to delete a cipher.
*/
suspend fun deleteCipher(cipherId: String): DeleteCipherResult
/**
* Attempt to update a cipher.
*/

View file

@ -30,6 +30,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
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
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.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
@ -348,6 +349,17 @@ class VaultRepositoryImpl(
)
}
override suspend fun deleteCipher(cipherId: String): DeleteCipherResult {
val userId = requireNotNull(activeUserId)
return ciphersService
.deleteCipher(cipherId)
.onSuccess { vaultDiskSource.deleteCipher(userId, cipherId) }
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
)
}
override suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,

View file

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

View file

@ -82,6 +82,18 @@ class VaultDiskSourceTest {
}
}
@Test
fun `DeleteCipher should call deleteCipher`() = runTest {
assertFalse(ciphersDao.deleteCipherCalled)
ciphersDao.storedCiphers.add(CIPHER_ENTITY)
assertEquals(1, ciphersDao.storedCiphers.size)
vaultDiskSource.deleteCipher(USER_ID, CIPHER_1.id)
assertTrue(ciphersDao.deleteCipherCalled)
assertEquals(emptyList<CipherEntity>(), ciphersDao.storedCiphers)
}
@Test
fun `saveCollection should call insertCollection`() = runTest {
assertFalse(collectionsDao.insertCollectionCalled)

View file

@ -9,6 +9,7 @@ class FakeCiphersDao : CiphersDao {
val storedCiphers = mutableListOf<CipherEntity>()
var deleteCipherCalled: Boolean = false
var deleteCiphersCalled: Boolean = false
var insertCiphersCalled: Boolean = false
@ -26,6 +27,14 @@ class FakeCiphersDao : CiphersDao {
return count
}
override suspend fun deleteCipher(userId: String, cipherId: String): Int {
deleteCipherCalled = true
val count = storedCiphers.count { it.userId == userId && it.id == cipherId }
storedCiphers.removeAll { it.userId == userId && it.id == cipherId }
ciphersFlow.tryEmit(storedCiphers.toList())
return count
}
override fun getAllCiphers(userId: String): Flow<List<CipherEntity>> =
ciphersFlow.map { ciphers -> ciphers.filter { it.userId == userId } }

View file

@ -7,8 +7,8 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import retrofit2.create
class CiphersServiceTest : BaseServiceTest() {
@ -63,6 +63,14 @@ class CiphersServiceTest : BaseServiceTest() {
result.getOrThrow(),
)
}
@Test
fun `deleteCipher should execute the delete cipher API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val cipherId = "cipherId"
val result = ciphersService.deleteCipher(cipherId = cipherId)
assertEquals(Unit, result.getOrThrow())
}
}
private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """

View file

@ -49,6 +49,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
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
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.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
@ -1390,6 +1391,36 @@ class VaultRepositoryTest {
assertEquals(UpdateCipherResult.Success, result)
}
@Suppress("MaxLineLength")
@Test
fun `deleteCipher with ciphersService deleteCipher failure should return DeleteCipherResult Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val cipherId = "mockId-1"
coEvery {
ciphersService.deleteCipher(cipherId = cipherId)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.deleteCipher(cipherId)
assertEquals(DeleteCipherResult.Error, result)
}
@Suppress("MaxLineLength")
@Test
fun `deleteCipher with ciphersService deleteCipher success should return DeleteCipherResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val cipherId = "mockId-1"
coEvery { ciphersService.deleteCipher(cipherId = cipherId) } returns Unit.asSuccess()
coEvery { vaultDiskSource.deleteCipher(userId, cipherId) } just runs
val result = vaultRepository.deleteCipher(cipherId)
assertEquals(DeleteCipherResult.Success, result)
}
@Test
fun `createSend with encryptSend failure should return CreateSendResult failure`() =
runTest {