mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-05-15 09:50:41 +00:00
feat(auth): Add support for password validation type (main, secondary).
This commit is contained in:
committed by
MargeBot
parent
e7e5df5010
commit
be82f85ca2
+2
@@ -60,6 +60,7 @@ import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.ProtonTypography
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.presentation.report.PasswordPolicyReport
|
||||
|
||||
@Composable
|
||||
@@ -215,6 +216,7 @@ private fun BackupPasswordChangeForm(
|
||||
ProtonTextFieldError(errorText = errorMsg)
|
||||
}
|
||||
PasswordPolicyReport(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
password = backupPassword,
|
||||
userId = userId,
|
||||
onResult = { isPasswordValid = it != null },
|
||||
|
||||
+2
@@ -69,6 +69,7 @@ import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.util.formatBold
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.presentation.report.PasswordPolicyReport
|
||||
|
||||
private val CompanyLogoSize = 56.dp
|
||||
@@ -288,6 +289,7 @@ private fun BackupPasswordSetupForm(
|
||||
ProtonTextFieldError(errorText = errorMsg)
|
||||
}
|
||||
PasswordPolicyReport(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
password = backupPassword,
|
||||
userId = userId,
|
||||
onResult = { isPasswordValid = it != null },
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ import me.proton.core.auth.presentation.databinding.FragmentSignupChoosePassword
|
||||
import me.proton.core.auth.presentation.util.setTextWithAnnotatedLink
|
||||
import me.proton.core.auth.presentation.viewmodel.signup.SignupViewModel
|
||||
import me.proton.core.observability.domain.metrics.SignupScreenViewTotalV1
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.presentation.report.PasswordPolicyReport
|
||||
import me.proton.core.presentation.utils.hideKeyboard
|
||||
import me.proton.core.presentation.utils.launchOnScreenView
|
||||
@@ -96,6 +97,7 @@ class ChoosePasswordFragment : SignupFragment(R.layout.fragment_signup_choose_pa
|
||||
|
||||
passwordPolicies.setContent {
|
||||
PasswordPolicyReport(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
passwordFlow = passwordInput.textChanges().map { it.toString() },
|
||||
userId = null,
|
||||
onResult = { isPasswordValid = it != null }
|
||||
|
||||
+6
-1
@@ -34,6 +34,7 @@ import me.proton.core.passvalidator.data.entity.PasswordValidatorTokenImpl
|
||||
import me.proton.core.passvalidator.data.validator.CommonPasswordValidator
|
||||
import me.proton.core.passvalidator.data.validator.MinLengthPasswordValidator
|
||||
import me.proton.core.passvalidator.data.validator.PasswordValidator
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.domain.usecase.ValidatePassword
|
||||
import me.proton.core.presentation.utils.InvalidPasswordProvider
|
||||
import me.proton.core.util.kotlin.DispatcherProvider
|
||||
@@ -80,9 +81,13 @@ public class ValidatePasswordImpl internal constructor(
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
passwordValidationType: PasswordValidationType,
|
||||
password: String,
|
||||
userId: UserId?
|
||||
): Flow<ValidatePassword.Result> = observePasswordPolicyValidators(userId)
|
||||
): Flow<ValidatePassword.Result> = when (passwordValidationType) {
|
||||
PasswordValidationType.Main -> observePasswordPolicyValidators(userId)
|
||||
PasswordValidationType.Secondary -> flowOf(emptyList())
|
||||
}
|
||||
.catchAll(LogTag.FETCH_PASS_POLICY) { emit(emptyList()) }
|
||||
.map { policyValidators -> policyValidators.takeIfNotEmpty() ?: listOf(defaultValidator) }
|
||||
.combine(getRequiredValidators(userId), Collection<PasswordValidator>::plus)
|
||||
|
||||
+24
-4
@@ -16,6 +16,7 @@ import me.proton.core.auth.domain.feature.IsCommonPasswordCheckEnabled
|
||||
import me.proton.core.passvalidator.data.validator.CommonPasswordValidator
|
||||
import me.proton.core.passvalidator.data.validator.MinLengthPasswordValidator
|
||||
import me.proton.core.passvalidator.data.validator.PasswordPolicyValidator
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorResult
|
||||
import me.proton.core.presentation.utils.InvalidPasswordProvider
|
||||
import me.proton.core.test.kotlin.TestDispatcherProvider
|
||||
@@ -67,7 +68,7 @@ class ValidatePasswordImplTest {
|
||||
every { minLengthPasswordValidator.validate(any()) } returns validatorResult()
|
||||
|
||||
// WHEN
|
||||
tested("password", null).test {
|
||||
tested(passwordValidationType = PasswordValidationType.Main, "password", null).test {
|
||||
val result = awaitItem()
|
||||
|
||||
// THEN
|
||||
@@ -91,7 +92,7 @@ class ValidatePasswordImplTest {
|
||||
every { isCommonPasswordCheckEnabled(null) } returns true
|
||||
|
||||
// WHEN
|
||||
tested("password", null).test {
|
||||
tested(passwordValidationType = PasswordValidationType.Main, "password", null).test {
|
||||
val result = awaitItem()
|
||||
|
||||
// THEN
|
||||
@@ -115,7 +116,7 @@ class ValidatePasswordImplTest {
|
||||
every { minLengthPasswordValidator.validate(any()) } returns validatorResult(isValid = false)
|
||||
|
||||
// WHEN
|
||||
tested("password", null).test {
|
||||
tested(passwordValidationType = PasswordValidationType.Main, "password", null).test {
|
||||
val result = awaitItem()
|
||||
|
||||
// THEN
|
||||
@@ -138,7 +139,7 @@ class ValidatePasswordImplTest {
|
||||
every { isCommonPasswordCheckEnabled(null) } returns true
|
||||
|
||||
// WHEN
|
||||
tested("password", null).test {
|
||||
tested(passwordValidationType = PasswordValidationType.Main, "password", null).test {
|
||||
val result = awaitItem()
|
||||
|
||||
// THEN
|
||||
@@ -153,6 +154,25 @@ class ValidatePasswordImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default validators for secondary password`() = runTest(dispatcherProvider.Main) {
|
||||
// GIVEN
|
||||
every { isCommonPasswordCheckEnabled(null) } returns false
|
||||
every { minLengthPasswordValidator.validate(any()) } returns validatorResult()
|
||||
|
||||
// WHEN
|
||||
tested(passwordValidationType = PasswordValidationType.Secondary, "password", null).test {
|
||||
val result = awaitItem()
|
||||
|
||||
// THEN
|
||||
assertEquals(1, result.results.size)
|
||||
assertNotNull(result.token)
|
||||
awaitComplete()
|
||||
|
||||
verify(exactly = 1) { minLengthPasswordValidator.validate(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatorResult(isOptional: Boolean = false, isValid: Boolean = true) = PasswordValidatorResult(
|
||||
errorMessage = "",
|
||||
hideIfValid = false,
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.passvalidator.domain.entity
|
||||
|
||||
public enum class PasswordValidationType {
|
||||
/** Validation for the main login password. */
|
||||
Main,
|
||||
|
||||
/** Validation for the secondary (mailbox) password. */
|
||||
Secondary
|
||||
}
|
||||
+6
-1
@@ -20,11 +20,16 @@ package me.proton.core.passvalidator.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorResult
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorToken
|
||||
|
||||
public interface ValidatePassword {
|
||||
public operator fun invoke(password: String, userId: UserId?): Flow<Result>
|
||||
public operator fun invoke(
|
||||
passwordValidationType: PasswordValidationType,
|
||||
password: String,
|
||||
userId: UserId?
|
||||
): Flow<Result>
|
||||
|
||||
public data class Result(
|
||||
val results: List<PasswordValidatorResult>,
|
||||
|
||||
+5
-1
@@ -47,6 +47,7 @@ import me.proton.core.compose.theme.captionHint
|
||||
import me.proton.core.compose.theme.captionWeak
|
||||
import me.proton.core.compose.viewmodel.hiltViewModelOrNull
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorToken
|
||||
import me.proton.core.passvalidator.presentation.R
|
||||
|
||||
@@ -71,6 +72,7 @@ public fun LegacyPasswordPolicyReportStyle(): PasswordPolicyReportStyle = Passwo
|
||||
|
||||
@Composable
|
||||
public fun PasswordPolicyReport(
|
||||
passwordValidationType: PasswordValidationType,
|
||||
passwordFlow: Flow<String>,
|
||||
userId: UserId?,
|
||||
onResult: (token: PasswordValidatorToken?) -> Unit,
|
||||
@@ -81,6 +83,7 @@ public fun PasswordPolicyReport(
|
||||
val password by passwordFlow.collectAsStateWithLifecycle(initialValue = "")
|
||||
|
||||
PasswordPolicyReport(
|
||||
passwordValidationType = passwordValidationType,
|
||||
password = password,
|
||||
userId = userId,
|
||||
onResult = onResult,
|
||||
@@ -92,6 +95,7 @@ public fun PasswordPolicyReport(
|
||||
|
||||
@Composable
|
||||
public fun PasswordPolicyReport(
|
||||
passwordValidationType: PasswordValidationType,
|
||||
password: String,
|
||||
userId: UserId?,
|
||||
onResult: (token: PasswordValidatorToken?) -> Unit,
|
||||
@@ -102,7 +106,7 @@ public fun PasswordPolicyReport(
|
||||
val state by viewModel?.state?.collectAsStateWithLifecycle() ?: return
|
||||
|
||||
LaunchedEffect(password) {
|
||||
viewModel?.perform(PasswordPolicyReportAction.Validate(password, userId))
|
||||
viewModel?.perform(PasswordPolicyReportAction.Validate(passwordValidationType, password, userId))
|
||||
}
|
||||
|
||||
PasswordPolicyReport(
|
||||
|
||||
+6
-1
@@ -19,10 +19,15 @@
|
||||
package me.proton.core.passvalidator.presentation.report
|
||||
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
|
||||
public sealed interface PasswordPolicyReportOperation
|
||||
|
||||
public sealed interface PasswordPolicyReportAction : PasswordPolicyReportOperation {
|
||||
public data object NoOp : PasswordPolicyReportAction
|
||||
public data class Validate(val password: String, val userId: UserId?) : PasswordPolicyReportAction
|
||||
public data class Validate(
|
||||
val passwordValidationType: PasswordValidationType,
|
||||
val password: String,
|
||||
val userId: UserId?
|
||||
) : PasswordPolicyReportAction
|
||||
}
|
||||
|
||||
+12
-2
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.proton.core.compose.viewmodel.BaseViewModel
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorResult
|
||||
import me.proton.core.passvalidator.domain.usecase.ValidatePassword
|
||||
import javax.inject.Inject
|
||||
@@ -38,7 +39,11 @@ public open class PasswordPolicyReportViewModel @Inject constructor(
|
||||
) {
|
||||
override fun onAction(action: PasswordPolicyReportAction): Flow<PasswordPolicyReportState> = when (action) {
|
||||
is PasswordPolicyReportAction.NoOp -> emptyFlow()
|
||||
is PasswordPolicyReportAction.Validate -> onValidate(action.password, action.userId)
|
||||
is PasswordPolicyReportAction.Validate -> onValidate(
|
||||
passwordValidationType = action.passwordValidationType,
|
||||
password = action.password,
|
||||
userId = action.userId
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun FlowCollector<PasswordPolicyReportState>.onError(throwable: Throwable) {
|
||||
@@ -46,9 +51,14 @@ public open class PasswordPolicyReportViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onValidate(
|
||||
passwordValidationType: PasswordValidationType,
|
||||
password: String,
|
||||
userId: UserId?
|
||||
): Flow<PasswordPolicyReportState> = validatePassword(password, userId).map { (validationResults, token) ->
|
||||
): Flow<PasswordPolicyReportState> = validatePassword(
|
||||
passwordValidationType = passwordValidationType,
|
||||
password = password,
|
||||
userId = userId
|
||||
).map { (validationResults, token) ->
|
||||
when {
|
||||
validationResults.isEmpty() -> PasswordPolicyReportState.Hidden(token = token)
|
||||
else -> {
|
||||
|
||||
+38
-8
@@ -7,6 +7,7 @@ import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorResult
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidatorToken
|
||||
import me.proton.core.passvalidator.domain.usecase.ValidatePassword
|
||||
@@ -33,7 +34,12 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
@Test
|
||||
fun `empty results`() = runTest {
|
||||
// GIVEN
|
||||
every { validatePassword(any(), any()) } returns flowOf(ValidatePassword.Result(emptyList(), FakeToken()))
|
||||
every { validatePassword(any(), any(), any()) } returns flowOf(
|
||||
ValidatePassword.Result(
|
||||
emptyList(),
|
||||
FakeToken()
|
||||
)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
tested.state.test {
|
||||
@@ -41,7 +47,13 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
assertIs<PasswordPolicyReportState.Loading>(awaitItem())
|
||||
|
||||
// WHEN
|
||||
tested.perform(PasswordPolicyReportAction.Validate(password = "password", userId = null))
|
||||
tested.perform(
|
||||
PasswordPolicyReportAction.Validate(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
password = "password",
|
||||
userId = null
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertIs<PasswordPolicyReportState.Hidden>(awaitItem())
|
||||
@@ -52,7 +64,7 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
@Test
|
||||
fun `validation results`() = runTest {
|
||||
// GIVEN
|
||||
every { validatePassword(any(), any()) } answers {
|
||||
every { validatePassword(any(), any(), any()) } answers {
|
||||
flowOf(
|
||||
ValidatePassword.Result(emptyList(), null),
|
||||
ValidatePassword.Result(testValidatorResults, null)
|
||||
@@ -65,7 +77,13 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
assertIs<PasswordPolicyReportState.Loading>(awaitItem())
|
||||
|
||||
// WHEN
|
||||
tested.perform(PasswordPolicyReportAction.Validate(password = "password", userId = null))
|
||||
tested.perform(
|
||||
PasswordPolicyReportAction.Validate(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
password = "password",
|
||||
userId = null
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
awaitItem().let {
|
||||
@@ -90,7 +108,7 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
@Test
|
||||
fun `hint with error`() = runTest {
|
||||
// GIVEN
|
||||
every { validatePassword(any(), any()) } answers {
|
||||
every { validatePassword(any(), any(), any()) } answers {
|
||||
flowOf(
|
||||
ValidatePassword.Result(emptyList(), FakeToken()),
|
||||
ValidatePassword.Result(smallResults, FakeToken())
|
||||
@@ -103,7 +121,13 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
assertIs<PasswordPolicyReportState.Loading>(awaitItem())
|
||||
|
||||
// WHEN
|
||||
tested.perform(PasswordPolicyReportAction.Validate(password = "password", userId = null))
|
||||
tested.perform(
|
||||
PasswordPolicyReportAction.Validate(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
password = "password",
|
||||
userId = null
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
awaitItem().let {
|
||||
@@ -123,7 +147,7 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
|
||||
@Test
|
||||
fun `runtime error`() = runTest {
|
||||
every { validatePassword(any(), any()) } returns flow {
|
||||
every { validatePassword(any(), any(), any()) } returns flow {
|
||||
error("unexpected error")
|
||||
}
|
||||
|
||||
@@ -133,7 +157,13 @@ class PasswordPolicyReportViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
assertIs<PasswordPolicyReportState.Loading>(awaitItem())
|
||||
|
||||
// WHEN
|
||||
tested.perform(PasswordPolicyReportAction.Validate(password = "password", userId = null))
|
||||
tested.perform(
|
||||
PasswordPolicyReportAction.Validate(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
password = "password",
|
||||
userId = null
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
awaitItem().let {
|
||||
|
||||
+2
@@ -45,6 +45,7 @@ import me.proton.core.auth.presentation.viewmodel.Source
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.network.presentation.util.getUserMessage
|
||||
import me.proton.core.passvalidator.domain.entity.PasswordValidationType
|
||||
import me.proton.core.passvalidator.presentation.report.PasswordPolicyReport
|
||||
import me.proton.core.presentation.ui.ProtonSecureFragment
|
||||
import me.proton.core.presentation.ui.view.ProtonInput
|
||||
@@ -149,6 +150,7 @@ class PasswordManagementFragment :
|
||||
|
||||
newLoginPasswordPolicies.setContent {
|
||||
PasswordPolicyReport(
|
||||
passwordValidationType = PasswordValidationType.Main,
|
||||
passwordFlow = newLoginPasswordInput.textChanges().map { it.toString() },
|
||||
userId = userId,
|
||||
onResult = { isNewPasswordValid = it != null }
|
||||
|
||||
Reference in New Issue
Block a user