PM-45475: bug: Sync before updating password to ensure correct policies are in place

This commit is contained in:
David Perez
2026-11-04 17:13:23 -06:00
parent 512650c477
commit 9da0750edf
6 changed files with 628 additions and 292 deletions
@@ -33,7 +33,10 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.bitwarden.ui.platform.components.appbar.model.OverflowMenuItemData
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.button.model.BitwardenButtonData
import com.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
@@ -42,8 +45,10 @@ import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.bitwarden.ui.platform.components.util.throttledClick
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
@@ -113,11 +118,13 @@ fun ResetPasswordScreen(
navigationIcon = null,
scrollBehavior = scrollBehavior,
actions = {
BitwardenTextButton(
label = stringResource(id = BitwardenString.save),
onClick = { viewModel.trySendAction(ResetPasswordAction.SaveClick) },
modifier = Modifier.testTag("SaveButton"),
)
if (state.viewState.shouldShowSaveButton) {
BitwardenTextButton(
label = stringResource(id = BitwardenString.save),
onClick = { viewModel.trySendAction(ResetPasswordAction.SaveClick) },
modifier = Modifier.testTag("SaveButton"),
)
}
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
OverflowMenuItemData(
@@ -130,32 +137,56 @@ fun ResetPasswordScreen(
)
},
) {
ResetPasswordScreenContent(
state = state,
onCurrentPasswordInputChanged = {
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(it))
},
onPasswordInputChanged = {
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(it))
},
onRetypePasswordInputChanged = {
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(it))
},
onPasswordHintInputChanged = {
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged(it))
},
onLearnToPreventLockout = {
viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick)
},
modifier = Modifier.fillMaxSize(),
)
when (val viewState = state.viewState) {
is ResetPasswordState.ViewState.Content -> {
ResetPasswordScreenContent(
state = viewState,
onCurrentPasswordInputChanged = {
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(it))
},
onPasswordInputChanged = {
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(it))
},
onRetypePasswordInputChanged = {
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(it))
},
onPasswordHintInputChanged = {
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged(it))
},
onLearnToPreventLockout = {
viewModel.trySendAction(ResetPasswordAction.LearnHowPreventLockoutClick)
},
modifier = Modifier.fillMaxSize(),
)
}
is ResetPasswordState.ViewState.Error -> {
BitwardenErrorContent(
message = viewState.message(),
buttonData = BitwardenButtonData(
label = BitwardenString.try_again.asText(),
onClick = throttledClick {
viewModel.trySendAction(ResetPasswordAction.RetryClick)
},
),
modifier = Modifier.fillMaxSize(),
)
}
ResetPasswordState.ViewState.Loading -> {
BitwardenLoadingContent(
text = stringResource(id = BitwardenString.syncing),
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
@Composable
@Suppress("LongMethod")
private fun ResetPasswordScreenContent(
state: ResetPasswordState,
state: ResetPasswordState.ViewState.Content,
onCurrentPasswordInputChanged: (String) -> Unit,
onPasswordInputChanged: (String) -> Unit,
onRetypePasswordInputChanged: (String) -> Unit,
@@ -258,7 +289,7 @@ private fun ResetPasswordScreenContent(
Column {
Text(
text = stringResource(
BitwardenString
id = BitwardenString
.bitwarden_cannot_reset_a_lost_or_forgotten_master_password,
),
style = BitwardenTheme.typography.bodySmall,
@@ -292,16 +323,15 @@ private fun ResetPasswordScreenContent(
private fun ResetPasswordScreenContent_preview() {
BitwardenTheme {
ResetPasswordScreenContent(
state = ResetPasswordState(
policies = listOf(),
state = ResetPasswordState.ViewState.Content(
resetReason = null,
dialogState = null,
policies = persistentListOf(),
minimumPasswordLength = 12,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.GOOD,
minimumPasswordLength = 12,
),
onCurrentPasswordInputChanged = {},
onPasswordInputChanged = {},
@@ -17,13 +17,13 @@ import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.util.toDisplayLabels
import com.x8bit.bitwarden.ui.tools.feature.generator.util.toStrictestPolicy
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@@ -39,38 +39,17 @@ private const val MIN_PASSWORD_LENGTH = 12
@Suppress("TooManyFunctions")
class ResetPasswordViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<ResetPasswordState, ResetPasswordEvent, ResetPasswordAction>(
initialState = savedStateHandle[KEY_STATE]
?: run {
val policies = authRepository.passwordPolicies
ResetPasswordState(
policies = policies.toDisplayLabels(),
resetReason = authRepository.passwordResetReason,
dialogState = null,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
minimumPasswordLength = policies
.toStrictestPolicy()
.minLength
?: MIN_PASSWORD_LENGTH,
)
},
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: ResetPasswordState(
viewState = ResetPasswordState.ViewState.Loading,
dialogState = null,
),
) {
/**
* Keeps track of async request to get password strength. Should be cancelled
* when user input changes.
*/
private var passwordStrengthJob: Job = Job().apply { complete() }
init {
// As state updates, write to saved state handle.
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
syncVault()
}
override fun handleAction(action: ResetPasswordAction) {
@@ -78,7 +57,7 @@ class ResetPasswordViewModel @Inject constructor(
ResetPasswordAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
ResetPasswordAction.SaveClick -> handleSaveClicked()
ResetPasswordAction.DialogDismiss -> handleDialogDismiss()
ResetPasswordAction.RetryClick -> handleRetryClick()
is ResetPasswordAction.CurrentPasswordInputChanged -> {
handleCurrentPasswordInputChanged(action)
}
@@ -93,6 +72,7 @@ class ResetPasswordViewModel @Inject constructor(
handlePasswordHintInputChanged(action)
}
is ResetPasswordAction.Internal.ReceiveSyncResult -> handleReceiveSyncResult(action)
is ResetPasswordAction.Internal.ReceiveResetPasswordResult -> {
handleReceiveResetPasswordResult(action)
}
@@ -105,60 +85,62 @@ class ResetPasswordViewModel @Inject constructor(
handleReceiveValidatePasswordResult(action)
}
is ResetPasswordAction.Internal.ReceivePasswordStrengthResult -> {
handlePasswordStrengthResult(action)
}
ResetPasswordAction.LearnHowPreventLockoutClick -> {
handleLearnHowToPreventLockoutClick()
}
}
}
private fun handleLearnHowToPreventLockoutClick() {
sendEvent(ResetPasswordEvent.NavigateToPreventAccountLockout)
}
private fun checkPasswordStrength(input: String) {
// Update password strength:
passwordStrengthJob.cancel()
if (input.isEmpty()) {
mutableStateFlow.update {
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
}
} else {
passwordStrengthJob = viewModelScope.launch {
val result = authRepository.getPasswordStrength(
password = input,
)
trySendAction(ResetPasswordAction.Internal.ReceivePasswordStrengthResult(result))
}
private fun handleRetryClick() {
mutableStateFlow.update {
it.copy(viewState = ResetPasswordState.ViewState.Loading)
}
syncVault()
}
private fun handlePasswordStrengthResult(
action: ResetPasswordAction.Internal.ReceivePasswordStrengthResult,
) {
when (val result = action.result) {
is PasswordStrengthResult.Success -> {
val updatedState = when (result.passwordStrength) {
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
}
/**
* Display the content if the sync succeeded, refreshing the policies in case they changed,
* and otherwise display an error.
*/
private fun handleReceiveSyncResult(action: ResetPasswordAction.Internal.ReceiveSyncResult) {
when (action.result) {
is SyncVaultDataResult.Error -> {
mutableStateFlow.update {
it.copy(
passwordStrengthState = updatedState,
viewState = ResetPasswordState.ViewState.Error(
message = BitwardenString.generic_error_message.asText(),
),
)
}
}
is PasswordStrengthResult.Error -> Unit
is SyncVaultDataResult.Success -> {
val policies = authRepository.passwordPolicies
mutableStateFlow.update {
it.copy(
viewState = ResetPasswordState.ViewState.Content(
resetReason = authRepository.passwordResetReason,
policies = policies.toDisplayLabels(),
minimumPasswordLength = policies
.toStrictestPolicy()
.minLength
?: MIN_PASSWORD_LENGTH,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
),
)
}
}
}
}
private fun handleLearnHowToPreventLockoutClick() {
sendEvent(ResetPasswordEvent.NavigateToPreventAccountLockout)
}
/**
* Dismiss the view if the user confirms logging out.
*/
@@ -170,51 +152,67 @@ class ResetPasswordViewModel @Inject constructor(
* Validate the user's current password when they submit.
*/
private fun handleSaveClicked() {
// Display an error dialog if the new password field is blank.
if (state.passwordInput.isBlank()) {
mutableStateFlow.update {
it.copy(
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.validation_field_required
.asText(BitwardenString.master_password.asText()),
),
)
}
return
}
// Check if the new password meets the policy requirements, if applicable.
if (state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
viewModelScope.launch {
val result = authRepository.validatePasswordAgainstPolicies(state.passwordInput)
sendAction(
ResetPasswordAction.Internal.ReceiveValidatePasswordAgainstPoliciesResult(
result,
),
)
}
} else {
// Otherwise, simply verify that the password meets the minimum length requirement.
if (state.passwordInput.length < MIN_PASSWORD_LENGTH) {
onContent { content ->
// Display an error dialog if the new password field is blank.
if (content.passwordInput.isBlank()) {
mutableStateFlow.update {
it.copy(
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenPlurals.master_password_length_val_message_x
.asPluralsText(
quantity = MIN_PASSWORD_LENGTH,
args = arrayOf(MIN_PASSWORD_LENGTH),
),
message = BitwardenString
.validation_field_required
.asText(BitwardenString.master_password.asText()),
),
)
}
} else {
// Check that the re-typed password matches.
if (!checkRetypedPassword()) return
return@onContent
}
// Otherwise, if the password checks out, attempt to reset it.
resetPassword()
when (content.resetReason) {
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN -> {
// Check if the new password meets the policy requirements, if applicable.
viewModelScope.launch {
val result = authRepository.validatePasswordAgainstPolicies(
password = content.passwordInput,
)
sendAction(
ResetPasswordAction
.Internal
.ReceiveValidatePasswordAgainstPoliciesResult(
meetsRequirements = result,
),
)
}
}
ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
null,
-> {
// Otherwise, simply verify that the password meets the
// minimum length requirement.
if (content.passwordInput.length < MIN_PASSWORD_LENGTH) {
mutableStateFlow.update {
it.copy(
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenPlurals
.master_password_length_val_message_x
.asPluralsText(
quantity = MIN_PASSWORD_LENGTH,
args = arrayOf(MIN_PASSWORD_LENGTH),
),
),
)
}
} else {
// Check that the re-typed password matches.
if (!checkRetypedPassword(content = content)) return@onContent
// Otherwise, if the password checks out, attempt to reset it.
resetPassword()
}
}
}
}
}
@@ -224,9 +222,7 @@ class ResetPasswordViewModel @Inject constructor(
*/
private fun handleDialogDismiss() {
mutableStateFlow.update {
it.copy(
dialogState = null,
)
it.copy(dialogState = null)
}
}
@@ -236,10 +232,8 @@ class ResetPasswordViewModel @Inject constructor(
private fun handleCurrentPasswordInputChanged(
action: ResetPasswordAction.CurrentPasswordInputChanged,
) {
mutableStateFlow.update {
it.copy(
currentPasswordInput = action.input,
)
updateContent {
it.copy(currentPasswordInput = action.input)
}
}
@@ -247,12 +241,32 @@ class ResetPasswordViewModel @Inject constructor(
* Update the state with the new password input.
*/
private fun handlePasswordInputChanged(action: ResetPasswordAction.PasswordInputChanged) {
mutableStateFlow.update {
it.copy(
passwordInput = action.input,
)
updateContent { content ->
val input = action.input
if (input.isEmpty()) {
content.copy(
passwordStrengthState = PasswordStrengthState.NONE,
passwordInput = input,
)
} else {
when (val result = authRepository.getPasswordStrength(password = input)) {
is PasswordStrengthResult.Success -> {
content.copy(
passwordStrengthState = when (result.passwordStrength) {
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
},
passwordInput = input,
)
}
is PasswordStrengthResult.Error -> content.copy(passwordInput = input)
}
}
}
checkPasswordStrength(input = action.input)
}
/**
@@ -261,10 +275,8 @@ class ResetPasswordViewModel @Inject constructor(
private fun handleRetypePasswordInputChanged(
action: ResetPasswordAction.RetypePasswordInputChanged,
) {
mutableStateFlow.update {
it.copy(
retypePasswordInput = action.input,
)
updateContent {
it.copy(retypePasswordInput = action.input)
}
}
@@ -274,10 +286,8 @@ class ResetPasswordViewModel @Inject constructor(
private fun handlePasswordHintInputChanged(
action: ResetPasswordAction.PasswordHintInputChanged,
) {
mutableStateFlow.update {
it.copy(
passwordHintInput = action.input,
)
updateContent {
it.copy(passwordHintInput = action.input)
}
}
@@ -369,14 +379,15 @@ class ResetPasswordViewModel @Inject constructor(
return
}
// Check that the re-typed password matches.
if (!checkRetypedPassword()) return
onContent { content ->
// Check that the re-typed password matches.
if (!checkRetypedPassword(content = content)) return@onContent
// Check that the entered current password is correct.
viewModelScope.launch {
val currentPassword = state.currentPasswordInput
val result = authRepository.validatePassword(currentPassword)
trySendAction(ResetPasswordAction.Internal.ReceiveValidatePasswordResult(result))
// Check that the entered current password is correct.
viewModelScope.launch {
val result = authRepository.validatePassword(content.currentPasswordInput)
trySendAction(ResetPasswordAction.Internal.ReceiveValidatePasswordResult(result))
}
}
}
@@ -384,8 +395,10 @@ class ResetPasswordViewModel @Inject constructor(
* A helper function to determine if the re-typed password matches and
* display an alert if not. Returns true if the passwords match.
*/
private fun checkRetypedPassword(): Boolean {
if (state.passwordInput == state.retypePasswordInput) return true
private fun checkRetypedPassword(
content: ResetPasswordState.ViewState.Content,
): Boolean {
if (content.passwordInput == content.retypePasswordInput) return true
mutableStateFlow.update {
it.copy(
@@ -398,29 +411,56 @@ class ResetPasswordViewModel @Inject constructor(
return false
}
/**
* A helper function to launch the vault sync request.
*/
private fun syncVault() {
viewModelScope.launch {
val result = vaultRepository.syncForResult()
sendAction(ResetPasswordAction.Internal.ReceiveSyncResult(result))
}
}
/**
* A helper function to launch the reset password request.
*/
private fun resetPassword() {
// Show the loading dialog.
mutableStateFlow.update {
it.copy(
dialogState = ResetPasswordState.DialogState.Loading(
message = BitwardenString.updating_password.asText(),
),
)
}
viewModelScope.launch {
val result = authRepository.resetPassword(
currentPassword = state.currentPasswordInput.orNullIfBlank(),
newPassword = state.passwordInput,
passwordHint = state.passwordHintInput,
)
trySendAction(
ResetPasswordAction.Internal.ReceiveResetPasswordResult(result),
)
onContent { content ->
// Show the loading dialog.
mutableStateFlow.update {
it.copy(
dialogState = ResetPasswordState.DialogState.Loading(
message = BitwardenString.updating_password.asText(),
),
)
}
viewModelScope.launch {
val result = authRepository.resetPassword(
currentPassword = content.currentPasswordInput.orNullIfBlank(),
newPassword = content.passwordInput,
passwordHint = content.passwordHintInput,
)
trySendAction(ResetPasswordAction.Internal.ReceiveResetPasswordResult(result))
}
}
}
private inline fun onContent(
crossinline block: (ResetPasswordState.ViewState.Content) -> Unit,
) {
(state.viewState as? ResetPasswordState.ViewState.Content)?.let(block)
}
private inline fun updateContent(
crossinline block: (
ResetPasswordState.ViewState.Content,
) -> ResetPasswordState.ViewState.Content,
) {
val updatedContent = (state.viewState as? ResetPasswordState.ViewState.Content)
?.let(block)
?: return
mutableStateFlow.update { it.copy(viewState = updatedContent) }
}
}
/**
@@ -428,16 +468,54 @@ class ResetPasswordViewModel @Inject constructor(
*/
@Parcelize
data class ResetPasswordState(
val policies: List<Text>,
val resetReason: ForcePasswordResetReason?,
val viewState: ViewState,
val dialogState: DialogState?,
val currentPasswordInput: String,
val passwordInput: String,
val retypePasswordInput: String,
val passwordHintInput: String,
val passwordStrengthState: PasswordStrengthState,
val minimumPasswordLength: Int,
) : Parcelable {
/**
* Represents the specific view states for the Reset Password screen.
*/
sealed class ViewState : Parcelable {
/**
* Indicates if the save button should be displayed.
*/
abstract val shouldShowSaveButton: Boolean
/**
* Represents a loading state while the vault is syncing.
*/
@Parcelize
data object Loading : ViewState() {
override val shouldShowSaveButton: Boolean get() = false
}
/**
* Represents an error state with the given [message].
*/
@Parcelize
data class Error(
val message: Text,
) : ViewState() {
override val shouldShowSaveButton: Boolean get() = false
}
/**
* Represents the loaded content state.
*/
@Parcelize
data class Content(
val resetReason: ForcePasswordResetReason?,
val policies: ImmutableList<Text>,
val minimumPasswordLength: Int,
val currentPasswordInput: String,
val passwordInput: String,
val retypePasswordInput: String,
val passwordHintInput: String,
val passwordStrengthState: PasswordStrengthState,
) : ViewState() {
override val shouldShowSaveButton: Boolean get() = true
}
}
/**
* Represents the current state of any dialogs on the screen.
*/
@@ -493,6 +571,11 @@ sealed class ResetPasswordAction {
*/
data object DialogDismiss : ResetPasswordAction()
/**
* Indicates that the user has clicked the retry button after a failed sync.
*/
data object RetryClick : ResetPasswordAction()
/**
* Indicates that the current password input has changed.
*/
@@ -522,6 +605,13 @@ sealed class ResetPasswordAction {
* Models actions that the [ResetPasswordViewModel] might send itself.
*/
sealed class Internal : ResetPasswordAction() {
/**
* Indicates that a vault sync result has been received.
*/
data class ReceiveSyncResult(
val result: SyncVaultDataResult,
) : Internal()
/**
* Indicates that a reset password result has been received.
*/
@@ -542,12 +632,5 @@ sealed class ResetPasswordAction {
data class ReceiveValidatePasswordAgainstPoliciesResult(
val meetsRequirements: Boolean,
) : Internal()
/**
* Indicates a password strength result has been received.
*/
data class ReceivePasswordStrengthResult(
val result: PasswordStrengthResult,
) : Internal()
}
}
@@ -4,12 +4,14 @@ import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* Convert a list of master password policies into a list of text instructions
* for the user about what requirements the password must meet.
*/
fun List<PolicyInformation.MasterPassword>.toDisplayLabels(): List<Text> {
fun List<PolicyInformation.MasterPassword>.toDisplayLabels(): ImmutableList<Text> {
val list = mutableListOf<Text>()
mapNotNull { it.minLength }.maxOrNull()?.let {
@@ -36,5 +38,5 @@ fun List<PolicyInformation.MasterPassword>.toDisplayLabels(): List<Text> {
list.add(BitwardenString.policy_in_effect_special.asText())
}
return list
return list.toImmutableList()
}
@@ -25,6 +25,7 @@ import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue
@@ -52,6 +53,39 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `loading view state should display the loading content and hide the save button`() {
mutableStateFlow.update { it.copy(viewState = ResetPasswordState.ViewState.Loading) }
composeTestRule.onNodeWithText("Syncing…").assertIsDisplayed()
composeTestRule.onNodeWithText("Save").assertDoesNotExist()
composeTestRule
.onNodeWithText("Re-type new master password (required)")
.assertDoesNotExist()
}
@Test
fun `error view state should display the error content and hide the save button`() {
mutableStateFlow.update {
it.copy(viewState = ResetPasswordState.ViewState.Error(message = "Fail".asText()))
}
composeTestRule.onNodeWithText("Fail").assertIsDisplayed()
composeTestRule.onNodeWithText("Try again").assertIsDisplayed()
composeTestRule.onNodeWithText("Save").assertDoesNotExist()
}
@Test
fun `try again click in error state should send RetryClick action`() {
mutableStateFlow.update {
it.copy(viewState = ResetPasswordState.ViewState.Error(message = "Fail".asText()))
}
composeTestRule.onNodeWithText("Try again").performClick()
verify { viewModel.trySendAction(ResetPasswordAction.RetryClick) }
}
@Test
fun `basicDialog should update according to state`() {
composeTestRule
@@ -120,6 +154,7 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
fun `submit button click should emit SubmitClick`() {
composeTestRule
.onNodeWithText("Save")
.assertIsDisplayed()
.performClick()
verify {
@@ -140,7 +175,9 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
mutableStateFlow.update {
it.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
viewState = DEFAULT_CONTENT.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
),
)
}
@@ -165,7 +202,9 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
mutableStateFlow.update {
it.copy(
policies = listOf("Make password better".asText()),
viewState = DEFAULT_CONTENT.copy(
policies = persistentListOf("Make password better".asText()),
),
)
}
@@ -180,7 +219,9 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
mutableStateFlow.update {
it.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
viewState = DEFAULT_CONTENT.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
),
)
}
@@ -208,7 +249,9 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
mutableStateFlow.update {
it.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
viewState = DEFAULT_CONTENT.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
),
)
}
@@ -289,14 +332,18 @@ class ResetPasswordScreenTest : BitwardenComposeTest() {
}
}
private val DEFAULT_STATE = ResetPasswordState(
policies = emptyList(),
private val DEFAULT_CONTENT = ResetPasswordState.ViewState.Content(
resetReason = ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
dialogState = null,
policies = persistentListOf(),
minimumPasswordLength = 12,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
minimumPasswordLength = 12,
)
private val DEFAULT_STATE = ResetPasswordState(
viewState = DEFAULT_CONTENT,
dialogState = null,
)
@@ -12,13 +12,17 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordAction
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordEvent
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordState
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordViewModel
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.util.toDisplayLabels
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -26,6 +30,8 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -36,7 +42,129 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
every { passwordResetReason } returns ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN
}
private val savedStateHandle = SavedStateHandle()
private val vaultRepository: VaultRepository = mockk {
coEvery { syncForResult() } returns SyncVaultDataResult.Success(itemsAvailable = true)
}
@Test
fun `initial state should be Loading and transition to Content on sync success`() = runTest {
val syncResult = CompletableDeferred<SyncVaultDataResult>()
coEvery { vaultRepository.syncForResult() } coAnswers { syncResult.await() }
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(viewState = ResetPasswordState.ViewState.Loading),
awaitItem(),
)
syncResult.complete(SyncVaultDataResult.Success(itemsAvailable = true))
assertEquals(DEFAULT_STATE, awaitItem())
}
coVerify(exactly = 1) { vaultRepository.syncForResult() }
}
@Test
fun `sync success should populate the content from the latest policies and reset reason`() {
val policy = PolicyInformation.MasterPassword(
minLength = 20,
minComplexity = null,
requireUpper = null,
requireLower = null,
requireNumbers = null,
requireSpecial = null,
enforceOnLogin = true,
)
every { authRepository.passwordPolicies } returns listOf(policy)
every {
authRepository.passwordResetReason
} returns ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET
val viewModel = createViewModel()
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
policies = listOf(policy).toDisplayLabels(),
minimumPasswordLength = 20,
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `sync error should display the error view state`() {
coEvery {
vaultRepository.syncForResult()
} returns SyncVaultDataResult.Error(throwable = Throwable("Fail!"))
val viewModel = createViewModel()
assertEquals(
DEFAULT_STATE.copy(
viewState = ResetPasswordState.ViewState.Error(
message = BitwardenString.generic_error_message.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `RetryClick should display Loading and sync again`() = runTest {
coEvery {
vaultRepository.syncForResult()
} returns SyncVaultDataResult.Error(throwable = Throwable("Fail!"))
val viewModel = createViewModel()
val syncResult = CompletableDeferred<SyncVaultDataResult>()
coEvery { vaultRepository.syncForResult() } coAnswers { syncResult.await() }
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
viewState = ResetPasswordState.ViewState.Error(
message = BitwardenString.generic_error_message.asText(),
),
),
awaitItem(),
)
viewModel.trySendAction(ResetPasswordAction.RetryClick)
assertEquals(
DEFAULT_STATE.copy(viewState = ResetPasswordState.ViewState.Loading),
awaitItem(),
)
syncResult.complete(SyncVaultDataResult.Success(itemsAvailable = true))
assertEquals(DEFAULT_STATE, awaitItem())
}
coVerify(exactly = 2) { vaultRepository.syncForResult() }
}
@Test
fun `input and save actions should be ignored when not in the Content state`() {
coEvery {
vaultRepository.syncForResult()
} returns SyncVaultDataResult.Error(throwable = Throwable("Fail!"))
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_4)
val viewModel = createViewModel()
val errorState = DEFAULT_STATE.copy(
viewState = ResetPasswordState.ViewState.Error(
message = BitwardenString.generic_error_message.asText(),
),
)
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
viewModel.trySendAction(ResetPasswordAction.SaveClick)
assertEquals(errorState, viewModel.stateFlow.value)
}
@Test
fun `ConfirmLogoutClick logs out`() = runTest {
@@ -59,7 +187,9 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
currentPasswordInput = "Test123",
viewState = DEFAULT_CONTENT.copy(
currentPasswordInput = "Test123",
),
),
viewModel.stateFlow.value,
)
@@ -92,10 +222,10 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
@Test
fun `SubmitClicked with invalid password shows error alert for weak password reason`() {
val password = "Test123"
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
every {
authRepository.validatePasswordAgainstPolicies(password = password)
} returns false
coEvery {
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
@@ -105,12 +235,14 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.master_password_policy_validation_title.asText(),
message = BitwardenString.master_password_policy_validation_message.asText(),
),
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -122,7 +254,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
every {
authRepository.passwordResetReason
} returns ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET
coEvery {
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
@@ -132,7 +264,11 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
viewState = DEFAULT_CONTENT.copy(
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText(
@@ -140,8 +276,6 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
args = arrayOf(MIN_PASSWORD_LENGTH),
),
),
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -150,10 +284,10 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
@Test
fun `SubmitClicked with non-matching retyped password shows error alert`() {
val password = "Test123"
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
every {
authRepository.validatePasswordAgainstPolicies(password = password)
} returns true
coEvery {
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
@@ -164,12 +298,14 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.master_password_confirmation_val_message.asText(),
),
passwordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -180,13 +316,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
val currentPassword = "CurrentTest123"
val password = "Test123"
val error = Throwable("Fail!")
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
every {
authRepository.validatePasswordAgainstPolicies(password = password)
} returns true
coEvery {
authRepository.validatePassword(currentPassword)
} returns ValidatePasswordResult.Error(error = error)
coEvery {
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
@@ -199,15 +335,17 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.generic_error_message.asText(),
error = error,
),
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -217,13 +355,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
fun `SubmitClicked with invalid current password shows alert`() {
val currentPassword = "CurrentTest123"
val password = "Test123"
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
every {
authRepository.validatePasswordAgainstPolicies(password = password)
} returns true
coEvery {
authRepository.validatePassword(currentPassword)
} returns ValidatePasswordResult.Success(isValid = false)
coEvery {
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
@@ -236,14 +374,16 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = ResetPasswordState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.invalid_master_password.asText(),
),
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
viewModel.stateFlow.value,
)
@@ -253,8 +393,8 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
fun `SubmitClicked with all valid inputs resets password`() = runTest {
val currentPassword = "CurrentTest123"
val password = "Test123"
coEvery {
authRepository.validatePasswordAgainstPolicies(password)
every {
authRepository.validatePasswordAgainstPolicies(password = password)
} returns true
coEvery {
authRepository.validatePassword(currentPassword)
@@ -262,7 +402,7 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
coEvery {
authRepository.resetPassword(any(), any(), any())
} returns ResetPasswordResult.Success
coEvery {
every {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_0)
@@ -274,11 +414,13 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = null,
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
@@ -287,24 +429,28 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = ResetPasswordState.DialogState.Loading(
message = BitwardenString.updating_password.asText(),
),
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
dialogState = null,
currentPasswordInput = currentPassword,
passwordInput = password,
retypePasswordInput = password,
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
@@ -315,38 +461,55 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `PasswordInputChanged should update the password input in the state along with the password strength state`() =
runTest {
val passwordInput = "Test123"
val viewModel = createViewModel()
coEvery {
authRepository.getPasswordStrength(password = any())
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_4)
fun `PasswordInputChanged should update the password input in the state along with the password strength state`() {
val passwordInput = "Test123"
every {
authRepository.getPasswordStrength(password = passwordInput)
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_4)
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = passwordInput))
assertEquals(
DEFAULT_STATE.copy(
passwordInput = passwordInput,
),
awaitItem(),
)
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = passwordInput))
assertEquals(
DEFAULT_STATE.copy(
passwordInput = passwordInput,
passwordStrengthState = PasswordStrengthState.STRONG,
),
awaitItem(),
)
}
coVerify {
authRepository.getPasswordStrength(
password = passwordInput,
)
}
}
assertEquals(
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT.copy(
passwordInput = passwordInput,
passwordStrengthState = PasswordStrengthState.STRONG,
),
),
viewModel.stateFlow.value,
)
verify(exactly = 1) { authRepository.getPasswordStrength(password = passwordInput) }
}
@Test
fun `PasswordInputChanged with empty input should reset the password strength state`() {
every {
authRepository.getPasswordStrength(password = "a")
} returns PasswordStrengthResult.Success(passwordStrength = PasswordStrength.LEVEL_4)
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = "a"))
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = ""))
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
verify(exactly = 0) { authRepository.getPasswordStrength(password = "") }
}
@Test
fun `PasswordInputChanged with password strength error should not update the strength`() {
every {
authRepository.getPasswordStrength(password = "Test123")
} returns PasswordStrengthResult.Error(error = Throwable("Fail!"))
val viewModel = createViewModel()
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(input = "Test123"))
assertEquals(
DEFAULT_STATE.copy(viewState = DEFAULT_CONTENT.copy(passwordInput = "Test123")),
viewModel.stateFlow.value,
)
}
@Test
fun `RetypePasswordInputChanged should update the retype password input in the state`() {
@@ -355,7 +518,9 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
retypePasswordInput = "Test123",
viewState = DEFAULT_CONTENT.copy(
retypePasswordInput = "Test123",
),
),
viewModel.stateFlow.value,
)
@@ -368,7 +533,9 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
passwordHintInput = "Test123",
viewState = DEFAULT_CONTENT.copy(
passwordHintInput = "Test123",
),
),
viewModel.stateFlow.value,
)
@@ -387,22 +554,28 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
}
}
private fun createViewModel(): ResetPasswordViewModel =
private fun createViewModel(
initialState: ResetPasswordState? = null,
): ResetPasswordViewModel =
ResetPasswordViewModel(
authRepository = authRepository,
savedStateHandle = savedStateHandle,
vaultRepository = vaultRepository,
savedStateHandle = SavedStateHandle().apply { set("state", initialState) },
)
}
private const val MIN_PASSWORD_LENGTH = 12
private val DEFAULT_STATE = ResetPasswordState(
policies = emptyList(),
private val DEFAULT_CONTENT = ResetPasswordState.ViewState.Content(
resetReason = ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
dialogState = null,
policies = persistentListOf(),
minimumPasswordLength = MIN_PASSWORD_LENGTH,
currentPasswordInput = "",
passwordInput = "",
retypePasswordInput = "",
passwordHintInput = "",
passwordStrengthState = PasswordStrengthState.NONE,
minimumPasswordLength = 12,
)
private val DEFAULT_STATE = ResetPasswordState(
viewState = DEFAULT_CONTENT,
dialogState = null,
)
@@ -4,6 +4,7 @@ import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.model.createMockMasterPasswordPolicy
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.util.toDisplayLabels
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -16,7 +17,7 @@ class PolicyInformationMasterPasswordExtensionsTest {
createMockMasterPasswordPolicy(minLength = 2),
)
assertEquals(
listOf(BitwardenString.policy_in_effect_min_length.asText(10)),
persistentListOf(BitwardenString.policy_in_effect_min_length.asText(10)),
policyList.toDisplayLabels(),
)
}
@@ -29,7 +30,7 @@ class PolicyInformationMasterPasswordExtensionsTest {
createMockMasterPasswordPolicy(minComplexity = 2),
)
assertEquals(
listOf(BitwardenString.policy_in_effect_min_complexity.asText(2)),
persistentListOf(BitwardenString.policy_in_effect_min_complexity.asText(2)),
policyList.toDisplayLabels(),
)
}
@@ -43,7 +44,7 @@ class PolicyInformationMasterPasswordExtensionsTest {
createMockMasterPasswordPolicy(requireSpecial = true),
)
assertEquals(
listOf(
persistentListOf(
BitwardenString.policy_in_effect_uppercase.asText(),
BitwardenString.policy_in_effect_lowercase.asText(),
BitwardenString.policy_in_effect_numbers.asText(),