mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 11:18:45 +03:00
PM-7495 perform client side check for invalid MP before account deletion (#3439)
This commit is contained in:
parent
53c5d11076
commit
ed53abb29f
2 changed files with 93 additions and 21 deletions
|
@ -6,9 +6,11 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.DeleteAccountState.DeleteAccountDialog
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -22,6 +24,7 @@ private const val KEY_STATE = "state"
|
|||
/**
|
||||
* View model for the [DeleteAccountScreen].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class DeleteAccountViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
|
@ -49,9 +52,7 @@ class DeleteAccountViewModel @Inject constructor(
|
|||
is DeleteAccountAction.DeleteAccountClick -> handleDeleteAccountClick()
|
||||
DeleteAccountAction.AccountDeletionConfirm -> handleAccountDeletionConfirm()
|
||||
DeleteAccountAction.DismissDialog -> handleDismissDialog()
|
||||
is DeleteAccountAction.Internal.DeleteAccountComplete -> {
|
||||
handleDeleteAccountComplete(action)
|
||||
}
|
||||
is DeleteAccountAction.Internal -> handleInternalActions(action)
|
||||
|
||||
is DeleteAccountAction.DeleteAccountConfirmDialogClick -> {
|
||||
handleDeleteAccountConfirmDialogClick(action)
|
||||
|
@ -59,6 +60,16 @@ class DeleteAccountViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleInternalActions(action: DeleteAccountAction.Internal) {
|
||||
when (action) {
|
||||
is DeleteAccountAction.Internal.DeleteAccountComplete -> handleDeleteAccountComplete(
|
||||
action,
|
||||
)
|
||||
|
||||
is DeleteAccountAction.Internal.UpdateDialogState -> updateDialogState(action.dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteAccountClick() {
|
||||
sendEvent(DeleteAccountEvent.NavigateToDeleteAccountConfirmationScreen)
|
||||
}
|
||||
|
@ -74,22 +85,31 @@ class DeleteAccountViewModel @Inject constructor(
|
|||
private fun handleDeleteAccountConfirmDialogClick(
|
||||
action: DeleteAccountAction.DeleteAccountConfirmDialogClick,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = DeleteAccountState.DeleteAccountDialog.Loading)
|
||||
}
|
||||
updateDialogState(DeleteAccountDialog.Loading)
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.deleteAccountWithMasterPassword(action.masterPassword)
|
||||
sendAction(DeleteAccountAction.Internal.DeleteAccountComplete(result))
|
||||
val validPasswordResult = authRepository.validatePassword(action.masterPassword)
|
||||
if ((validPasswordResult as? ValidatePasswordResult.Success)?.isValid == false) {
|
||||
sendAction(
|
||||
DeleteAccountAction.Internal.UpdateDialogState(
|
||||
DeleteAccountDialog.Error(
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val result = authRepository.deleteAccountWithMasterPassword(action.masterPassword)
|
||||
sendAction(DeleteAccountAction.Internal.DeleteAccountComplete(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAccountDeletionConfirm() {
|
||||
authRepository.clearPendingAccountDeletion()
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
private fun handleDeleteAccountComplete(
|
||||
|
@ -97,23 +117,29 @@ class DeleteAccountViewModel @Inject constructor(
|
|||
) {
|
||||
when (val result = action.result) {
|
||||
DeleteAccountResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = DeleteAccountState.DeleteAccountDialog.DeleteSuccess)
|
||||
}
|
||||
updateDialogState(DeleteAccountDialog.DeleteSuccess)
|
||||
}
|
||||
|
||||
is DeleteAccountResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = DeleteAccountState.DeleteAccountDialog.Error(
|
||||
message = result.message?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateDialogState(
|
||||
DeleteAccountDialog.Error(
|
||||
message = result.message?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDialogState(dialog: DeleteAccountDialog?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
updateDialogState(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -225,5 +251,12 @@ sealed class DeleteAccountAction {
|
|||
data class DeleteAccountComplete(
|
||||
val result: DeleteAccountResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* An internal event to update the dialog state utilizing the synchronous action channel.
|
||||
*/
|
||||
data class UpdateDialogState(
|
||||
val dialog: DeleteAccountDialog,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.R
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -81,6 +82,9 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val masterPassword = "ckasb kcs ja"
|
||||
coEvery {
|
||||
authRepo.validatePassword(any())
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
coEvery {
|
||||
authRepo.deleteAccountWithMasterPassword(masterPassword)
|
||||
} returns DeleteAccountResult.Success
|
||||
|
@ -124,6 +128,9 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
|||
fun `on DeleteAccountClick should update dialog state when deleteAccount fails`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val masterPassword = "ckasb kcs ja"
|
||||
coEvery {
|
||||
authRepo.validatePassword(any())
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
coEvery {
|
||||
authRepo.deleteAccountWithMasterPassword(masterPassword)
|
||||
} returns DeleteAccountResult.Error(message = null)
|
||||
|
@ -144,6 +151,38 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DeleteAccountClick should update dialog state when invalid master pass is invalid`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val masterPassword = "ckasb kcs ja"
|
||||
coEvery {
|
||||
authRepo.validatePassword(any())
|
||||
} returns ValidatePasswordResult.Success(isValid = false)
|
||||
coEvery {
|
||||
authRepo.deleteAccountWithMasterPassword(masterPassword)
|
||||
} returns DeleteAccountResult.Error(message = null)
|
||||
|
||||
viewModel.trySendAction(
|
||||
DeleteAccountAction.DeleteAccountConfirmDialogClick(
|
||||
masterPassword
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialog = DeleteAccountState.DeleteAccountDialog.Error(
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
authRepo.deleteAccountWithMasterPassword(masterPassword)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AccountDeletionConfirm should clear dialog state and call clearPendingAccountDeletion`() =
|
||||
runTest {
|
||||
|
|
Loading…
Add table
Reference in a new issue