PM-7495 perform client side check for invalid MP before account deletion (#3439)

This commit is contained in:
Dave Severns 2024-07-15 13:51:50 -04:00 committed by GitHub
parent 53c5d11076
commit ed53abb29f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 21 deletions

View file

@ -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()
}
}

View file

@ -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 {