mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-506: Hard delete (#910)
This commit is contained in:
parent
087018bd26
commit
81d0e2f4db
5 changed files with 122 additions and 9 deletions
|
@ -40,7 +40,7 @@ interface CiphersDao {
|
|||
* 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")
|
||||
@Query("DELETE FROM ciphers WHERE user_id = :userId AND id = :cipherId")
|
||||
suspend fun deleteCipher(userId: String, cipherId: String): Int
|
||||
|
||||
/**
|
||||
|
|
|
@ -127,7 +127,7 @@ fun VaultItemScreen(
|
|||
if (pendingDeleteCipher) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.delete),
|
||||
message = stringResource(id = R.string.do_you_really_want_to_soft_delete_cipher),
|
||||
message = state.deletionConfirmationText(),
|
||||
confirmButtonText = stringResource(id = R.string.ok),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = {
|
||||
|
@ -182,7 +182,6 @@ fun VaultItemScreen(
|
|||
onClick = { pendingRestoreCipher = true },
|
||||
)
|
||||
}
|
||||
// TODO make action list dependent on item being in an organization BIT-1446
|
||||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOfNotNull(
|
||||
OverflowMenuItemData(
|
||||
|
|
|
@ -297,7 +297,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
R.string.soft_deleting.asText(),
|
||||
if (state.isCipherDeleted) {
|
||||
R.string.deleting.asText()
|
||||
} else {
|
||||
R.string.soft_deleting.asText()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -305,10 +309,16 @@ class VaultItemViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
VaultItemAction.Internal.DeleteCipherReceive(
|
||||
result = vaultRepository.softDeleteCipher(
|
||||
cipherId = state.vaultItemId,
|
||||
cipherView = cipher,
|
||||
),
|
||||
if (state.isCipherDeleted) {
|
||||
vaultRepository.hardDeleteCipher(
|
||||
cipherId = state.vaultItemId,
|
||||
)
|
||||
} else {
|
||||
vaultRepository.softDeleteCipher(
|
||||
cipherId = state.vaultItemId,
|
||||
cipherView = cipher,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -693,7 +703,15 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
DeleteCipherResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
sendEvent(VaultItemEvent.ShowToast(message = R.string.item_soft_deleted.asText()))
|
||||
sendEvent(
|
||||
VaultItemEvent.ShowToast(
|
||||
message = if (state.isCipherDeleted) {
|
||||
R.string.item_deleted.asText()
|
||||
} else {
|
||||
R.string.item_soft_deleted.asText()
|
||||
},
|
||||
),
|
||||
)
|
||||
sendEvent(VaultItemEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
@ -794,6 +812,17 @@ data class VaultItemState(
|
|||
?.isNotEmpty()
|
||||
?: false
|
||||
|
||||
/**
|
||||
* The text to display on the deletion confirmation dialog.
|
||||
*/
|
||||
val deletionConfirmationText: Text
|
||||
get() = if (isCipherDeleted) {
|
||||
R.string.do_you_really_want_to_permanently_delete_cipher
|
||||
} else {
|
||||
R.string.do_you_really_want_to_soft_delete_cipher
|
||||
}
|
||||
.asText()
|
||||
|
||||
/**
|
||||
* Represents the specific view states for the [VaultItemScreen].
|
||||
*/
|
||||
|
|
|
@ -660,6 +660,44 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Delete dialog text should display according to state`() {
|
||||
// Open the overflow menu
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("More")
|
||||
.performClick()
|
||||
// Click on the delete item in the dropdown
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Delete")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Do you really want to send to the trash?")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_LOGIN_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(
|
||||
currentCipher = createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "Do you really want to permanently delete? This cannot be undone.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Restore click should send show restore confirmation dialog`() {
|
||||
mutableStateFlow.update {
|
||||
|
|
|
@ -42,6 +42,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
|
@ -240,6 +241,52 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConfirmDeleteClick with deleted cipher should should invoke hardDeleteCipher`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON
|
||||
.copy(
|
||||
requiresReprompt = false,
|
||||
currentCipher = DEFAULT_COMMON
|
||||
.currentCipher
|
||||
?.copy(deletedDate = Instant.MIN),
|
||||
),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
coEvery {
|
||||
vaultRepo.hardDeleteCipher(
|
||||
cipherId = VAULT_ITEM_ID,
|
||||
)
|
||||
} returns DeleteCipherResult.Success
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.Common.ConfirmDeleteClick)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemEvent.ShowToast(R.string.item_deleted.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
coVerify { vaultRepo.hardDeleteCipher(cipherId = VAULT_ITEM_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() =
|
||||
|
|
Loading…
Add table
Reference in a new issue