diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt index 44504add93..ca804dc17d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt @@ -19,6 +19,11 @@ interface VaultDiskSource { */ fun getCiphers(userId: String): Flow> + /** + * 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]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt index cba8efcfac..7482318c74 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/CiphersDao.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/CiphersDao.kt index c238501009..b724f1d623 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/CiphersDao.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/CiphersDao.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt index ae9541d858..96a9708b17 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt @@ -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 + + /** + * Deletes a cipher. + */ + @DELETE("ciphers/{cipherId}") + suspend fun deleteCipher( + @Path("cipherId") cipherId: String, + ): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt index 68c181c272..840e214bf9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt @@ -20,4 +20,9 @@ interface CiphersService { cipherId: String, body: CipherJsonRequest, ): Result + + /** + * Attempt to delete a cipher. + */ + suspend fun deleteCipher(cipherId: String): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt index 272f82c6e0..a08b9f38f3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt @@ -34,4 +34,7 @@ class CiphersServiceImpl constructor( ) ?: throw throwable } + + override suspend fun deleteCipher(cipherId: String): Result = + ciphersApi.deleteCipher(cipherId = cipherId) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 5c7b720d36..b7165a2a58 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index f03b0bfcfd..52033f2b0c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteCipherResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteCipherResult.kt new file mode 100644 index 0000000000..ad63a25a56 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteCipherResult.kt @@ -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() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index bc9021bd6a..e5ed68ecb3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -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(), ciphersDao.storedCiphers) + } + @Test fun `saveCollection should call insertCollection`() = runTest { assertFalse(collectionsDao.insertCollectionCalled) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt index 1c7f7db218..665485f240 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt @@ -9,6 +9,7 @@ class FakeCiphersDao : CiphersDao { val storedCiphers = mutableListOf() + 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> = ciphersFlow.map { ciphers -> ciphers.filter { it.userId == userId } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt index f5e690ab88..82613e04e1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt @@ -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 = """ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 714c6f7ef2..04f2435be6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -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 {