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 GitHub
parent 4cd4110e89
commit a2655f6e74
11 changed files with 413 additions and 13 deletions

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"