BIT-707 Implement password strength indicator with mock values (#161)

This commit is contained in:
Andrew Haisting 2023-10-26 15:39:25 -05:00 committed by Álison Fernandes
parent 4ce7e0842b
commit 343e17f1f4
11 changed files with 413 additions and 13 deletions

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
@ -57,4 +58,9 @@ interface AuthRepository {
* Set the value of [captchaTokenResultFlow].
*/
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
/**
* Get the password strength for the given [email] and [password] combo.
*/
suspend fun getPasswordStrength(email: String, password: String): Result<PasswordStrength>
}

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
@ -18,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@ -225,4 +227,22 @@ class AuthRepositoryImpl @Inject constructor(
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
mutableCaptchaTokenFlow.tryEmit(tokenResult)
}
@Suppress("MagicNumber")
override suspend fun getPasswordStrength(
email: String,
password: String,
): Result<PasswordStrength> {
// TODO: Replace with SDK call (BIT-964)
// Ex: return authSdkSource.passwordStrength(email, password)
val length = password.length
return when {
length <= 3 -> PasswordStrength.LEVEL_0
length <= 6 -> PasswordStrength.LEVEL_1
length <= 9 -> PasswordStrength.LEVEL_2
length <= 11 -> PasswordStrength.LEVEL_3
else -> PasswordStrength.LEVEL_4
}
.asSuccess()
}
}

View file

@ -215,7 +215,19 @@ fun CreateAccountScreen(
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(4.dp))
Text(
text = state.passwordLengthLabel(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,

View file

@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
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.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
@ -14,16 +15,19 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Che
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ContinueWithBreachedPasswordClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Internal.ReceivePasswordStrengthResult
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
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.base.util.isValidEmail
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
@ -52,9 +56,16 @@ class CreateAccountViewModel @Inject constructor(
isAcceptPoliciesToggled = false,
isCheckDataBreachesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
),
) {
/**
* 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
@ -94,6 +105,24 @@ class CreateAccountViewModel @Inject constructor(
}
ContinueWithBreachedPasswordClick -> handleContinueWithBreachedPasswordClick()
is ReceivePasswordStrengthResult -> handlePasswordStrengthResult(action)
}
}
private fun handlePasswordStrengthResult(action: ReceivePasswordStrengthResult) {
action.result.onSuccess {
val updatedState = when (it) {
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
}
mutableStateFlow.update { oldState ->
oldState.copy(
passwordStrengthState = updatedState,
)
}
}
}
@ -203,7 +232,23 @@ class CreateAccountViewModel @Inject constructor(
}
private fun handlePasswordInputChanged(action: PasswordInputChange) {
// Update input:
mutableStateFlow.update { it.copy(passwordInput = action.input) }
// Update password strength:
passwordStrengthJob.cancel()
if (action.input.isEmpty()) {
mutableStateFlow.update {
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
}
} else {
passwordStrengthJob = viewModelScope.launch {
val result = authRepository.getPasswordStrength(
email = mutableStateFlow.value.emailInput,
password = action.input,
)
trySendAction(ReceivePasswordStrengthResult(result))
}
}
}
private fun handleConfirmPasswordInputChanged(action: ConfirmPasswordInputChange) {
@ -300,7 +345,14 @@ data class CreateAccountState(
val isCheckDataBreachesToggled: Boolean,
val isAcceptPoliciesToggled: Boolean,
val dialog: CreateAccountDialog?,
) : Parcelable
val passwordStrengthState: PasswordStrengthState,
) : Parcelable {
val passwordLengthLabel: Text
get() =
R.string.your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum
.asText(MIN_PASSWORD_LENGTH)
}
/**
* Models dialogs that can be displayed on the create account screen.
@ -445,5 +497,12 @@ sealed class CreateAccountAction {
data class ReceiveRegisterResult(
val registerResult: RegisterResult,
) : Internal()
/**
* Indicates a password strength result has been received.
*/
data class ReceivePasswordStrengthResult(
val result: Result<PasswordStrength>,
) : Internal()
}
}

View file

@ -0,0 +1,106 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
/**
* Draws a password indicator that displays password strength based on the given [state].
*/
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
@Composable
fun PasswordStrengthIndicator(
modifier: Modifier = Modifier,
state: PasswordStrengthState,
) {
val widthPercent by animateFloatAsState(
targetValue = when (state) {
PasswordStrengthState.NONE -> 0f
PasswordStrengthState.WEAK_1 -> .25f
PasswordStrengthState.WEAK_2 -> .5f
PasswordStrengthState.WEAK_3 -> .66f
PasswordStrengthState.GOOD -> .82f
PasswordStrengthState.STRONG -> 1f
},
label = "Width Percent State",
)
val indicatorColor = when (state) {
PasswordStrengthState.NONE -> MaterialTheme.colorScheme.error
PasswordStrengthState.WEAK_1 -> MaterialTheme.colorScheme.error
PasswordStrengthState.WEAK_2 -> MaterialTheme.colorScheme.error
PasswordStrengthState.WEAK_3 -> LocalNonMaterialColors.current.passwordWeak
PasswordStrengthState.GOOD -> MaterialTheme.colorScheme.primary
PasswordStrengthState.STRONG -> LocalNonMaterialColors.current.passwordStrong
}
val animatedIndicatorColor by animateColorAsState(
targetValue = indicatorColor,
label = "Indicator Color State",
)
val label = when (state) {
PasswordStrengthState.NONE -> "".asText()
PasswordStrengthState.WEAK_1 -> R.string.weak.asText()
PasswordStrengthState.WEAK_2 -> R.string.weak.asText()
PasswordStrengthState.WEAK_3 -> R.string.weak.asText()
PasswordStrengthState.GOOD -> R.string.good.asText()
PasswordStrengthState.STRONG -> R.string.strong.asText()
}
Column(
modifier = modifier,
) {
Box(
Modifier
.fillMaxWidth()
.height(4.dp)
.background(MaterialTheme.colorScheme.surfaceContainerHigh),
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.graphicsLayer {
transformOrigin = TransformOrigin(pivotFractionX = 0f, pivotFractionY = 0f)
scaleX = widthPercent
}
.drawBehind {
drawRect(animatedIndicatorColor)
},
)
}
Spacer(Modifier.height(4.dp))
Text(
text = label(),
style = MaterialTheme.typography.labelSmall,
color = indicatorColor,
)
}
}
/**
* Models various levels of password strength that can be displayed by [PasswordStrengthIndicator].
*/
enum class PasswordStrengthState {
NONE,
WEAK_1,
WEAK_2,
WEAK_3,
GOOD,
STRONG,
}

View file

@ -12,7 +12,10 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
@ -52,12 +55,20 @@ fun BitwardenTheme(
}
}
// Set overall theme based on color scheme and typography settings
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
val nonMaterialColors = if (darkTheme) {
darkNonMaterialColors(context)
} else {
lightNonMaterialColors(context)
}
CompositionLocalProvider(LocalNonMaterialColors provides nonMaterialColors) {
// Set overall theme based on color scheme and typography settings
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
}
private fun darkColorScheme(context: Context): ColorScheme =
@ -135,3 +146,33 @@ private fun lightColorScheme(context: Context): ColorScheme =
@ColorRes
private fun Int.toColor(context: Context): Color =
Color(context.getColor(this))
/**
* Provides access to non material theme colors throughout the app.
*/
val LocalNonMaterialColors: ProvidableCompositionLocal<NonMaterialColors> =
compositionLocalOf {
// Default value here will immediately be overridden in BitwardenTheme, similar
// to how MaterialTheme works.
NonMaterialColors(Color.Transparent, Color.Transparent)
}
/**
* Models colors that live outside of the Material Theme spec.
*/
data class NonMaterialColors(
val passwordWeak: Color,
val passwordStrong: Color,
)
private fun lightNonMaterialColors(context: Context): NonMaterialColors =
NonMaterialColors(
passwordWeak = R.color.light_password_strength_weak.toColor(context),
passwordStrong = R.color.light_password_strength_strong.toColor(context),
)
private fun darkNonMaterialColors(context: Context): NonMaterialColors =
NonMaterialColors(
passwordWeak = R.color.dark_password_strength_weak.toColor(context),
passwordStrong = R.color.dark_password_strength_strong.toColor(context),
)

View file

@ -104,4 +104,8 @@
<color name="gray">@color/grey_738182</color>
<color name="light_gray">@color/grey_EFEFF4</color>
<color name="white">@color/white_FFFFFF</color>
<color name="light_password_strength_weak">@color/orange_B27400</color>
<color name="dark_password_strength_weak">@color/orange_C9914F</color>
<color name="light_password_strength_strong">@color/green_009A38</color>
<color name="dark_password_strength_strong">@color/green_41B06D</color>
</resources>

View file

@ -62,5 +62,9 @@
<!-- Misc Colors -->
<color name="silver_DDDDDD">#dddddd</color>
<color name="black_000000">#000000</color>
<color name="orange_B27400">#FFB27400</color>
<color name="orange_C9914F">#FFC9914F</color>
<color name="green_009A38">#FF009A38</color>
<color name="green_41B06D">#FF41B06D</color>
</resources>

View file

@ -17,6 +17,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_0
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_1
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_2
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
@ -643,6 +648,27 @@ class AuthRepositoryTest {
}
}
@Test
fun `getPasswordStrength should be based on password length`() = runTest {
// TODO: Replace with SDK call (BIT-964)
assertEquals(LEVEL_0.asSuccess(), repository.getPasswordStrength(EMAIL, "1"))
assertEquals(LEVEL_0.asSuccess(), repository.getPasswordStrength(EMAIL, "12"))
assertEquals(LEVEL_0.asSuccess(), repository.getPasswordStrength(EMAIL, "123"))
assertEquals(LEVEL_1.asSuccess(), repository.getPasswordStrength(EMAIL, "1234"))
assertEquals(LEVEL_1.asSuccess(), repository.getPasswordStrength(EMAIL, "12345"))
assertEquals(LEVEL_1.asSuccess(), repository.getPasswordStrength(EMAIL, "123456"))
assertEquals(LEVEL_2.asSuccess(), repository.getPasswordStrength(EMAIL, "1234567"))
assertEquals(LEVEL_2.asSuccess(), repository.getPasswordStrength(EMAIL, "12345678"))
assertEquals(LEVEL_2.asSuccess(), repository.getPasswordStrength(EMAIL, "123456789"))
assertEquals(LEVEL_3.asSuccess(), repository.getPasswordStrength(EMAIL, "123456789a"))
assertEquals(LEVEL_3.asSuccess(), repository.getPasswordStrength(EMAIL, "123456789ab"))
assertEquals(LEVEL_4.asSuccess(), repository.getPasswordStrength(EMAIL, "123456789abc"))
}
companion object {
private const val GET_TOKEN_RESPONSE_EXTENSIONS_PATH =
"com.x8bit.bitwarden.data.auth.repository.util.GetTokenResponseExtensionsKt"

View file

@ -33,6 +33,7 @@ import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue
import org.junit.Test
@ -408,6 +409,46 @@ class CreateAccountScreenTest : BaseComposeTest() {
composeTestRule.onNode(isDialog()).assertIsDisplayed()
}
@Test
fun `password strength should change as state changes`() {
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns mutableStateFlow
every { eventFlow } returns emptyFlow()
}
composeTestRule.setContent {
CreateAccountScreen(
onNavigateBack = {},
onNavigateToLogin = { _, _ -> },
viewModel = viewModel,
)
}
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_1)
}
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_2)
}
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.WEAK_3)
}
composeTestRule.onNodeWithText("Weak").assertIsDisplayed()
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.GOOD)
}
composeTestRule.onNodeWithText("Good").assertIsDisplayed()
mutableStateFlow.update {
DEFAULT_STATE.copy(passwordStrengthState = PasswordStrengthState.STRONG)
}
composeTestRule.onNodeWithText("Strong").assertIsDisplayed()
}
@Test
fun `toggling one password field visibility should toggle the other`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
@ -457,6 +498,7 @@ class CreateAccountScreenTest : BaseComposeTest() {
isCheckDataBreachesToggled = false,
isAcceptPoliciesToggled = false,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
)
}
}

View file

@ -5,13 +5,21 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import app.cash.turbine.turbineScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_0
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_1
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_2
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Internal.ReceivePasswordStrengthResult
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
@ -71,6 +79,7 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
isCheckDataBreachesToggled = false,
isAcceptPoliciesToggled = false,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
)
val handle = SavedStateHandle(mapOf("state" to savedState))
val viewModel = CreateAccountViewModel(
@ -129,13 +138,16 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
@Test
fun `SubmitClick with password below 12 chars should show password length dialog`() = runTest {
val input = "abcdefghikl"
coEvery {
mockAuthRepository.getPasswordStrength("test@test.com", input)
} returns Throwable().asFailure()
val viewModel = CreateAccountViewModel(
savedStateHandle = SavedStateHandle(),
authRepository = mockAuthRepository,
)
val input = "abcdefghikl"
viewModel.trySendAction(EmailInputChange(EMAIL))
viewModel.trySendAction(PasswordInputChange("abcdefghikl"))
viewModel.trySendAction(PasswordInputChange(input))
val expectedState = DEFAULT_STATE.copy(
emailInput = EMAIL,
passwordInput = input,
@ -154,11 +166,14 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
@Test
fun `SubmitClick with passwords not matching should show password match dialog`() = runTest {
val input = "testtesttesttest"
coEvery {
mockAuthRepository.getPasswordStrength("test@test.com", input)
} returns Throwable().asFailure()
val viewModel = CreateAccountViewModel(
savedStateHandle = SavedStateHandle(),
authRepository = mockAuthRepository,
)
val input = "testtesttesttest"
viewModel.trySendAction(EmailInputChange("test@test.com"))
viewModel.trySendAction(PasswordInputChange(input))
val expectedState = DEFAULT_STATE.copy(
@ -179,11 +194,14 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
@Test
fun `SubmitClick without policies accepted should show accept policies error`() = runTest {
val password = "testtesttesttest"
coEvery {
mockAuthRepository.getPasswordStrength("test@test.com", password)
} returns Throwable().asFailure()
val viewModel = CreateAccountViewModel(
savedStateHandle = SavedStateHandle(),
authRepository = mockAuthRepository,
)
val password = "testtesttesttest"
viewModel.trySendAction(EmailInputChange("test@test.com"))
viewModel.trySendAction(PasswordInputChange(password))
viewModel.trySendAction(ConfirmPasswordInputChange(password))
@ -483,7 +501,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
}
@Test
fun `PasswordInputChange update passwordInput`() = runTest {
fun `PasswordInputChange update passwordInput and call getPasswordStrength`() = runTest {
coEvery {
mockAuthRepository.getPasswordStrength("", "input")
} returns Result.failure(Throwable())
val viewModel = CreateAccountViewModel(
savedStateHandle = SavedStateHandle(),
authRepository = mockAuthRepository,
@ -492,6 +513,7 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE.copy(passwordInput = "input"), awaitItem())
}
coVerify { mockAuthRepository.getPasswordStrength("", "input") }
}
@Test
@ -518,6 +540,62 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `ReceivePasswordStrengthResult should update password strength state`() = runTest {
val viewModel = CreateAccountViewModel(
savedStateHandle = SavedStateHandle(),
authRepository = mockAuthRepository,
)
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
passwordStrengthState = PasswordStrengthState.NONE,
),
awaitItem(),
)
viewModel.trySendAction(ReceivePasswordStrengthResult(LEVEL_0.asSuccess()))
assertEquals(
DEFAULT_STATE.copy(
passwordStrengthState = PasswordStrengthState.WEAK_1,
),
awaitItem(),
)
viewModel.trySendAction(ReceivePasswordStrengthResult(LEVEL_1.asSuccess()))
assertEquals(
DEFAULT_STATE.copy(
passwordStrengthState = PasswordStrengthState.WEAK_2,
),
awaitItem(),
)
viewModel.trySendAction(ReceivePasswordStrengthResult(LEVEL_2.asSuccess()))
assertEquals(
DEFAULT_STATE.copy(
passwordStrengthState = PasswordStrengthState.WEAK_3,
),
awaitItem(),
)
viewModel.trySendAction(ReceivePasswordStrengthResult(LEVEL_3.asSuccess()))
assertEquals(
DEFAULT_STATE.copy(
passwordStrengthState = PasswordStrengthState.GOOD,
),
awaitItem(),
)
viewModel.trySendAction(ReceivePasswordStrengthResult(LEVEL_4.asSuccess()))
assertEquals(
DEFAULT_STATE.copy(
passwordStrengthState = PasswordStrengthState.STRONG,
),
awaitItem(),
)
}
}
companion object {
private const val PASSWORD = "longenoughtpassword"
private const val EMAIL = "test@test.com"
@ -529,6 +607,7 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
isCheckDataBreachesToggled = true,
isAcceptPoliciesToggled = false,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
)
private val VALID_INPUT_STATE = CreateAccountState(
passwordInput = PASSWORD,
@ -538,6 +617,7 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
isCheckDataBreachesToggled = false,
isAcceptPoliciesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.GOOD,
)
private const val LOGIN_RESULT_PATH =
"com.x8bit.bitwarden.data.auth.repository.util.CaptchaUtilsKt"