mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
BIT-707 Implement password strength indicator with mock values (#161)
This commit is contained in:
parent
4ce7e0842b
commit
343e17f1f4
11 changed files with 413 additions and 13 deletions
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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),
|
||||
)
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue