feat(auth): Add common pass check.

This commit is contained in:
dkadrikj 2024-04-23 09:35:31 +02:00
parent e4627b6ff3
commit 9115a3a20a
27 changed files with 10419 additions and 44 deletions

View File

@ -14,6 +14,7 @@ public final class me/proton/core/auth/dagger/BuildConfig {
}
public abstract interface class me/proton/core/auth/dagger/CoreAuthFeaturesModule {
public abstract fun bindIsCommonPasswordCheckEnabled (Lme/proton/core/auth/data/usecase/IsCommonPasswordCheckEnabledImpl;)Lme/proton/core/auth/domain/IsCommonPasswordCheckEnabled;
public abstract fun bindIsCredentialLessEnabled (Lme/proton/core/auth/data/usecase/IsCredentialLessEnabledImpl;)Lme/proton/core/auth/domain/usecase/IsCredentialLessEnabled;
public abstract fun provideIsSsoCustomTabEnabled (Lme/proton/core/auth/data/usecase/IsSsoCustomTabEnabledImpl;)Lme/proton/core/auth/domain/usecase/IsSsoCustomTabEnabled;
public abstract fun provideIsSsoEnabled (Lme/proton/core/auth/data/usecase/IsSsoEnabledImpl;)Lme/proton/core/auth/domain/usecase/IsSsoEnabled;

View File

@ -25,11 +25,13 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.proton.core.auth.data.usecase.IsCommonPasswordCheckEnabledImpl
import me.proton.core.auth.data.MissingScopeListenerImpl
import me.proton.core.auth.data.repository.AuthRepositoryImpl
import me.proton.core.auth.data.usecase.IsCredentialLessEnabledImpl
import me.proton.core.auth.data.usecase.IsSsoCustomTabEnabledImpl
import me.proton.core.auth.data.usecase.IsSsoEnabledImpl
import me.proton.core.auth.domain.IsCommonPasswordCheckEnabled
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.auth.domain.usecase.IsCredentialLessEnabled
import me.proton.core.auth.domain.usecase.IsSsoCustomTabEnabled
@ -74,4 +76,9 @@ public interface CoreAuthFeaturesModule {
@Binds
@Singleton
public fun bindIsCredentialLessEnabled(impl: IsCredentialLessEnabledImpl): IsCredentialLessEnabled
@Binds
@Singleton
public fun bindIsCommonPasswordCheckEnabled(impl: IsCommonPasswordCheckEnabledImpl): IsCommonPasswordCheckEnabled
}

View File

@ -631,6 +631,21 @@ public final class me/proton/core/auth/data/repository/AuthRepositoryImpl : me/p
public fun validatePhone (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class me/proton/core/auth/data/usecase/IsCommonPasswordCheckEnabledImpl : me/proton/core/auth/domain/IsCommonPasswordCheckEnabled {
public fun <init> (Landroid/content/Context;Lme/proton/core/featureflag/domain/FeatureFlagManager;)V
public fun invoke (Lme/proton/core/domain/entity/UserId;)Z
public fun isLocalEnabled ()Z
public fun isRemoteEnabled (Lme/proton/core/domain/entity/UserId;)Z
}
public final class me/proton/core/auth/data/usecase/IsCommonPasswordCheckEnabledImpl_Factory : dagger/internal/Factory {
public fun <init> (Ljavax/inject/Provider;Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/auth/data/usecase/IsCommonPasswordCheckEnabledImpl_Factory;
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Lme/proton/core/auth/data/usecase/IsCommonPasswordCheckEnabledImpl;
public static fun newInstance (Landroid/content/Context;Lme/proton/core/featureflag/domain/FeatureFlagManager;)Lme/proton/core/auth/data/usecase/IsCommonPasswordCheckEnabledImpl;
}
public final class me/proton/core/auth/data/usecase/IsCredentialLessEnabledImpl : me/proton/core/auth/domain/usecase/IsCredentialLessEnabled {
public fun <init> (Landroid/content/Context;Lme/proton/core/featureflag/domain/FeatureFlagManager;)V
public fun awaitIsRemoteDisabled-8Mi8wO0 (Lme/proton/core/domain/entity/UserId;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;

View File

@ -29,8 +29,8 @@ protonBuild {
}
protonCoverage {
branchCoveragePercentage.set(56)
lineCoveragePercentage.set(56)
branchCoveragePercentage.set(61)
lineCoveragePercentage.set(58)
}
publishOption.shouldBePublishedAsLib = true
@ -42,6 +42,7 @@ android {
dependencies {
api(
project(Module.authDomain),
project(Module.challengeData),
project(Module.challengeDomain),
project(Module.cryptoCommon),
project(Module.domain),
@ -55,7 +56,6 @@ dependencies {
)
implementation(
project(Module.challengeData),
project(Module.kotlinUtil),
`coroutines-core`,
)

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 Proton Technologies 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.auth.data.usecase
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.proton.core.auth.data.R
import me.proton.core.auth.domain.IsCommonPasswordCheckEnabled
import me.proton.core.domain.entity.UserId
import me.proton.core.featureflag.domain.ExperimentalProtonFeatureFlag
import me.proton.core.featureflag.domain.FeatureFlagManager
import me.proton.core.featureflag.domain.entity.FeatureId
import javax.inject.Inject
class IsCommonPasswordCheckEnabledImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val featureFlagManager: FeatureFlagManager
) : IsCommonPasswordCheckEnabled {
override fun invoke(userId: UserId?): Boolean {
return isLocalEnabled() && isRemoteEnabled(userId = userId)
}
override fun isLocalEnabled(): Boolean {
return context.resources.getBoolean(R.bool.core_feature_common_password_check_enabled)
}
@OptIn(ExperimentalProtonFeatureFlag::class)
override fun isRemoteEnabled(userId: UserId?): Boolean {
return featureFlagManager.getValue(userId = null, featureId)
}
internal companion object {
val featureId = FeatureId("SignUpCommonPasswordCheck")
}
}

View File

@ -20,4 +20,5 @@
<bool name="core_feature_auth_sso_enabled">true</bool>
<bool name="core_feature_auth_sso_custom_tab_enabled">true</bool>
<bool name="core_feature_credential_less_enabled">false</bool>
<bool name="core_feature_common_password_check_enabled">true</bool>
</resources>

View File

@ -3,4 +3,5 @@
<public name="core_feature_auth_sso_enabled" type="bool" />
<public name="core_feature_auth_sso_custom_tab_enabled" type="bool" />
<public name="core_feature_credential_less_enabled" type="bool" />
<public name="core_feature_common_password_check_enabled" type="bool" />
</resources>

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2024 Proton Technologies 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.auth.data.usecase
import android.content.Context
import android.content.res.Resources
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import me.proton.core.auth.data.R
import me.proton.core.featureflag.domain.ExperimentalProtonFeatureFlag
import me.proton.core.featureflag.domain.FeatureFlagManager
import org.junit.Assert.*
import kotlin.test.BeforeTest
import kotlin.test.Test
@OptIn(ExperimentalProtonFeatureFlag::class)
class IsCommonPasswordCheckEnabledImplTest {
@MockK
private lateinit var context: Context
@MockK
private lateinit var featureFlagManager: FeatureFlagManager
@MockK
private lateinit var resources: Resources
private val featureId = IsCommonPasswordCheckEnabledImpl.featureId
private lateinit var tested: IsCommonPasswordCheckEnabledImpl
@BeforeTest
fun setUp() {
MockKAnnotations.init(this)
every { context.resources } returns resources
every { featureFlagManager.getValue(any(), any()) } returns false
tested = IsCommonPasswordCheckEnabledImpl(context, featureFlagManager)
}
@Test
fun commonPasswordCheckEnabled() {
every { resources.getBoolean(R.bool.core_feature_common_password_check_enabled) } returns true
every { featureFlagManager.getValue(any(), featureId) } returns true
assertTrue(tested(null))
}
@Test
fun commonPasswordCheckDisabled() {
every { resources.getBoolean(R.bool.core_feature_common_password_check_enabled) } returns false
every { featureFlagManager.getValue(any(), featureId) } returns false
kotlin.test.assertFalse(tested(null))
}
@Test
fun commonPasswordCheckDisabledRemoteDisabled() {
every { resources.getBoolean(R.bool.core_feature_common_password_check_enabled) } returns true
every { featureFlagManager.getValue(any(), featureId) } returns false
kotlin.test.assertFalse(tested(null))
}
@Test
fun commonPasswordCheckDisabledLocalDisabled() {
every { resources.getBoolean(R.bool.core_feature_common_password_check_enabled) } returns false
every { featureFlagManager.getValue(any(), featureId) } returns true
kotlin.test.assertFalse(tested(null))
}
}

View File

@ -1,3 +1,6 @@
public abstract interface class me/proton/core/auth/domain/IsCommonPasswordCheckEnabled : me/proton/core/featureflag/domain/IsFeatureFlagEnabled {
}
public final class me/proton/core/auth/domain/LogTag {
public static final field INSTANCE Lme/proton/core/auth/domain/LogTag;
public static final field INVALID_SRP_PROOF Ljava/lang/String;

View File

@ -42,6 +42,7 @@ dependencies {
project(Module.challengeDomain),
project(Module.cryptoCommon),
project(Module.domain),
project(Module.featureFlagDomain),
project(Module.networkDomain),
project(Module.paymentDomain),
project(Module.planDomain),

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 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.auth.domain
import me.proton.core.featureflag.domain.IsFeatureFlagEnabled
interface IsCommonPasswordCheckEnabled : IsFeatureFlagEnabled

View File

@ -1815,6 +1815,7 @@ public abstract interface class me/proton/core/auth/presentation/ui/signup/Choos
public final class me/proton/core/auth/presentation/ui/signup/ChoosePasswordFragment : me/proton/core/auth/presentation/ui/signup/SignupFragment {
public fun <init> ()V
public fun onBackPressed ()V
public fun onCreate (Landroid/os/Bundle;)V
public fun onViewCreated (Landroid/view/View;Landroid/os/Bundle;)V
}
@ -1822,6 +1823,14 @@ public abstract interface class me/proton/core/auth/presentation/ui/signup/Choos
public abstract fun injectChoosePasswordFragment (Lme/proton/core/auth/presentation/ui/signup/ChoosePasswordFragment;)V
}
public final class me/proton/core/auth/presentation/ui/signup/ChoosePasswordFragment_MembersInjector : dagger/MembersInjector {
public fun <init> (Ljavax/inject/Provider;)V
public static fun create (Ljavax/inject/Provider;)Ldagger/MembersInjector;
public static fun injectIsCommonPasswordCheckEnabled (Lme/proton/core/auth/presentation/ui/signup/ChoosePasswordFragment;Lme/proton/core/auth/domain/IsCommonPasswordCheckEnabled;)V
public synthetic fun injectMembers (Ljava/lang/Object;)V
public fun injectMembers (Lme/proton/core/auth/presentation/ui/signup/ChoosePasswordFragment;)V
}
public final class me/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment : me/proton/core/auth/presentation/ui/signup/SignupFragment {
public static final field ARG_INPUT_CANCELLABLE Ljava/lang/String;
public static final field Companion Lme/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment$Companion;

View File

@ -51,6 +51,7 @@ dependencies {
project(Module.countryDomain),
project(Module.cryptoCommon),
project(Module.domain),
project(Module.featureFlagDomain),
project(Module.humanVerificationDomain),
project(Module.humanVerificationPresentation),
project(Module.networkData),
@ -58,8 +59,10 @@ dependencies {
project(Module.observabilityDomain),
project(Module.paymentDomain),
project(Module.paymentPresentation),
project(Module.planDomain),
project(Module.planPresentation),
project(Module.presentation),
project(Module.telemetryDomain),
project(Module.telemetryPresentation),
project(Module.userDomain),
project(Module.userSettingsDomain),
@ -72,12 +75,17 @@ dependencies {
`lifecycle-common`,
`lifecycle-savedState`,
lottie,
material
material,
okhttp,
`startup-runtime`
)
implementation(
project(Module.challengeData),
project(Module.countryPresentation),
project(Module.deviceUtil),
project(Module.kotlinUtil),
project(Module.sharedPreferencesUtil),
activity,
`androidx-browser`,
`android-ktx`,

View File

@ -21,17 +21,22 @@ package me.proton.core.auth.presentation.ui.signup
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.domain.IsCommonPasswordCheckEnabled
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.databinding.FragmentSignupChoosePasswordBinding
import me.proton.core.auth.presentation.viewmodel.signup.SignupViewModel
import me.proton.core.observability.domain.metrics.SignupScreenViewTotalV1
import me.proton.core.presentation.utils.InvalidPasswordProvider
import me.proton.core.presentation.utils.hideKeyboard
import me.proton.core.presentation.utils.launchOnScreenView
import me.proton.core.presentation.utils.onClick
import me.proton.core.presentation.utils.onFailure
import me.proton.core.presentation.utils.onSuccess
import me.proton.core.presentation.utils.validateInvalidPassword
import me.proton.core.presentation.utils.validatePasswordMatch
import me.proton.core.presentation.utils.validatePasswordMinLength
import me.proton.core.presentation.utils.viewBinding
@ -41,6 +46,7 @@ import me.proton.core.telemetry.presentation.annotation.ScreenClosed
import me.proton.core.telemetry.presentation.annotation.ScreenDisplayed
import me.proton.core.telemetry.presentation.annotation.ViewClicked
import me.proton.core.telemetry.presentation.annotation.ViewFocused
import javax.inject.Inject
@AndroidEntryPoint
@ProductMetrics(
@ -67,13 +73,24 @@ import me.proton.core.telemetry.presentation.annotation.ViewFocused
)
class ChoosePasswordFragment : SignupFragment(R.layout.fragment_signup_choose_password) {
@Inject
internal lateinit var isCommonPasswordCheckEnabled: IsCommonPasswordCheckEnabled
private val signupViewModel by activityViewModels<SignupViewModel>()
private val binding by viewBinding(FragmentSignupChoosePasswordBinding::bind)
private val invalidPasswordProvider by lazy { InvalidPasswordProvider(requireContext()) }
override fun onBackPressed() {
parentFragmentManager.popBackStack()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
invalidPasswordProvider.init()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -111,12 +128,26 @@ class ChoosePasswordFragment : SignupFragment(R.layout.fragment_signup_choose_pa
binding.passwordInput.apply {
val result = validatePasswordMinLength()
.onFailure { setInputError(getString(R.string.auth_signup_validation_password_length)) }
.onSuccess { password -> validateConfirmPasswordField(password) }
.onSuccess { _ -> validateInvalidPassword() }
signupViewModel.onInputValidationResult(result)
}
}
private fun validateConfirmPasswordField(password: String) = with(binding) {
private fun validateInvalidPassword() = with(binding) {
val onSuccess = { validateConfirmPasswordField() }
passwordInput.apply {
if (isCommonPasswordCheckEnabled(userId = null)) {
val result = validateInvalidPassword(invalidPasswordProvider)
.onFailure { setInputError(getString(R.string.auth_signup_password_not_allowed)) }
.onSuccess { _ -> onSuccess() }
signupViewModel.onInputValidationResult(result)
} else {
onSuccess()
}
}
}
private fun validateConfirmPasswordField() = with(binding) {
val confirmedPassword = confirmPasswordInput.text.toString()
val result = passwordInput.validatePasswordMatch(confirmedPassword)
.onFailure {

View File

@ -36,9 +36,9 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.auth.domain.usecase.signup.SetCreateAccountSuccess
import me.proton.core.auth.domain.usecase.signup.PerformCreateExternalEmailUser
import me.proton.core.auth.domain.usecase.signup.PerformCreateUser
import me.proton.core.auth.domain.usecase.signup.SetCreateAccountSuccess
import me.proton.core.auth.domain.usecase.signup.SignupChallengeConfig
import me.proton.core.auth.domain.usecase.userAlreadyExists
import me.proton.core.auth.presentation.entity.signup.RecoveryMethod

View File

@ -135,6 +135,7 @@
<string name="auth_signup_error_username_blank">Username must not be blank.</string>
<string name="auth_signup_error_passwords_do_not_match">Passwords do not match.</string>
<string name="auth_signup_validation_password_length">Password should be at least 8 characters long.</string>
<string name="auth_signup_password_not_allowed">Password not allowed.</string>
<string name="auth_signup_validation_password">Password must not be empty.</string>
<string name="auth_signup_your_account_is_being_setup">Your account is being created…</string>
<string name="auth_signup_please_wait">It usually takes no more than a minute.</string>

View File

@ -32,9 +32,9 @@ import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.auth.domain.usecase.GetPrimaryUser
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.auth.domain.usecase.signup.SetCreateAccountSuccess
import me.proton.core.auth.domain.usecase.signup.PerformCreateExternalEmailUser
import me.proton.core.auth.domain.usecase.signup.PerformCreateUser
import me.proton.core.auth.domain.usecase.signup.SetCreateAccountSuccess
import me.proton.core.auth.domain.usecase.signup.SignupChallengeConfig
import me.proton.core.auth.presentation.entity.signup.RecoveryMethod
import me.proton.core.auth.presentation.entity.signup.RecoveryMethodType

View File

@ -30,7 +30,7 @@ android {
}
dependencies {
implementation(
api(
project(Module.featureFlagData),
project(Module.featureFlagDomain)
)

View File

@ -45,14 +45,15 @@ dependencies {
project(Module.dataRoom),
project(Module.domain),
project(Module.featureFlagDomain),
project(Module.kotlinUtil),
project(Module.accountManagerDomain),
project(Module.observabilityDomain),
project(Module.networkData),
)
implementation(
project(Module.accountDomain),
project(Module.data),
project(Module.kotlinUtil),
project(Module.networkDomain),
`android-work-runtime`,

View File

@ -877,16 +877,18 @@ public final class me/proton/core/presentation/utils/InputValidationResult {
public static final field MAX_YEAR I
public static final field MIN_MONTH I
public static final field YEAR_2000 I
public fun <init> (Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;Lme/proton/core/presentation/utils/InvalidPasswordProvider;)V
public synthetic fun <init> (Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;Lme/proton/core/presentation/utils/InvalidPasswordProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lme/proton/core/presentation/utils/ValidationType;
public final fun component3 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;)Lme/proton/core/presentation/utils/InputValidationResult;
public static synthetic fun copy$default (Lme/proton/core/presentation/utils/InputValidationResult;Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/presentation/utils/InputValidationResult;
public final fun component4 ()Lme/proton/core/presentation/utils/InvalidPasswordProvider;
public final fun copy (Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;Lme/proton/core/presentation/utils/InvalidPasswordProvider;)Lme/proton/core/presentation/utils/InputValidationResult;
public static synthetic fun copy$default (Lme/proton/core/presentation/utils/InputValidationResult;Ljava/lang/String;Lme/proton/core/presentation/utils/ValidationType;Ljava/lang/String;Lme/proton/core/presentation/utils/InvalidPasswordProvider;ILjava/lang/Object;)Lme/proton/core/presentation/utils/InputValidationResult;
public fun equals (Ljava/lang/Object;)Z
public final fun getAdditionalText ()Ljava/lang/String;
public final fun getCardType ()Lme/proton/core/presentation/utils/CardType;
public final fun getProvider ()Lme/proton/core/presentation/utils/InvalidPasswordProvider;
public final fun getText ()Ljava/lang/String;
public final fun getValidationType ()Lme/proton/core/presentation/utils/ValidationType;
public fun hashCode ()I
@ -898,6 +900,17 @@ public final class me/proton/core/presentation/utils/InputValidationResult {
public final class me/proton/core/presentation/utils/InputValidationResult$Companion {
}
public final class me/proton/core/presentation/utils/InvalidPasswordProvider {
public static final field Companion Lme/proton/core/presentation/utils/InvalidPasswordProvider$Companion;
public static final field FILE_NAME_COMMON_PASSWORDS Ljava/lang/String;
public fun <init> (Landroid/content/Context;)V
public final fun init (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun isPasswordCommon (Ljava/lang/String;)Z
}
public final class me/proton/core/presentation/utils/InvalidPasswordProvider$Companion {
}
public final class me/proton/core/presentation/utils/LocaleUtilsKt {
public static final fun currentLocale (Landroid/content/res/Configuration;)Ljava/util/Locale;
}
@ -1137,6 +1150,7 @@ public final class me/proton/core/presentation/utils/ValidationType : java/lang/
public static final field CreditCardCVC Lme/proton/core/presentation/utils/ValidationType;
public static final field CreditCardExpirationDate Lme/proton/core/presentation/utils/ValidationType;
public static final field Email Lme/proton/core/presentation/utils/ValidationType;
public static final field InvalidPassword Lme/proton/core/presentation/utils/ValidationType;
public static final field NotBlank Lme/proton/core/presentation/utils/ValidationType;
public static final field Password Lme/proton/core/presentation/utils/ValidationType;
public static final field PasswordMatch Lme/proton/core/presentation/utils/ValidationType;
@ -1160,6 +1174,7 @@ public final class me/proton/core/presentation/utils/ValidationUtilsKt {
public static final fun validateCreditCardCVC (Lme/proton/core/presentation/ui/view/ProtonInput;)Lme/proton/core/presentation/utils/InputValidationResult;
public static final fun validateEmail (Lme/proton/core/presentation/ui/view/ProtonInput;)Lme/proton/core/presentation/utils/InputValidationResult;
public static final fun validateExpirationDate (Lme/proton/core/presentation/ui/view/ProtonInput;)Lme/proton/core/presentation/utils/InputValidationResult;
public static final fun validateInvalidPassword (Lme/proton/core/presentation/ui/view/ProtonInput;Lme/proton/core/presentation/utils/InvalidPasswordProvider;)Lme/proton/core/presentation/utils/InputValidationResult;
public static final fun validatePassword (Lme/proton/core/presentation/ui/view/ProtonInput;)Lme/proton/core/presentation/utils/InputValidationResult;
public static final fun validatePasswordMatch (Lme/proton/core/presentation/ui/view/ProtonInput;Ljava/lang/String;)Lme/proton/core/presentation/utils/InputValidationResult;
public static final fun validatePasswordMinLength (Lme/proton/core/presentation/ui/view/ProtonInput;)Lme/proton/core/presentation/utils/InputValidationResult;

View File

@ -43,9 +43,11 @@ android {
dependencies {
api(
project(Module.networkDomain),
activity,
appcompat,
`constraint-layout`,
coordinatorlayout,
`coroutines-core`,
fragment,
`javax-inject`,
@ -53,17 +55,19 @@ dependencies {
`lifecycle-savedState`,
`lifecycle-viewModel`,
material,
okhttp,
recyclerview,
)
implementation(
project(Module.kotlinUtil),
project(Module.networkData),
project(Module.networkDomain),
`android-ktx`,
`core-splashscreen`,
drawerLayout,
`hilt-android`,
`lifecycle-livedata-core`,
`lifecycle-runtime`,
`lifecycle-process`,
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2024 Proton Technologies 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.presentation.utils
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class InvalidPasswordProvider(private val context: Context) {
private var commonPasswords: Set<String>? = null
suspend fun init() {
readPasswords()
}
fun isPasswordCommon(password: String): Boolean = commonPasswords?.contains(password) ?: false
private suspend fun readPasswords() = withContext(Dispatchers.IO) {
if (commonPasswords == null) {
commonPasswords = context.readFromAssets(FILE_NAME_COMMON_PASSWORDS)
.split("\n")
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
}
}
private fun Context.readFromAssets(resource: String): String =
assets.open(resource).reader().use { it.readText() }
companion object {
const val FILE_NAME_COMMON_PASSWORDS = "ignis_10k.txt"
}
}

View File

@ -36,6 +36,9 @@ fun ProtonInput.validateUsername() =
fun ProtonInput.validatePassword() =
InputValidationResult(this.text.orEmpty(), ValidationType.Password)
fun ProtonInput.validateInvalidPassword(provider: InvalidPasswordProvider) =
InputValidationResult(text = this.text.orEmpty(), validationType = ValidationType.InvalidPassword, provider = provider)
fun ProtonInput.validatePasswordMinLength() =
InputValidationResult(this.text.orEmpty(), ValidationType.PasswordMinLength)
@ -58,6 +61,7 @@ enum class ValidationType(val minLong: Int = Int.MIN_VALUE, val maxLong: Int = I
NotBlank,
Username,
Password,
InvalidPassword,
PasswordMinLength(8),
PasswordMatch,
Email,
@ -77,7 +81,8 @@ enum class CardType(val regex: String) {
data class InputValidationResult(
val text: String,
val validationType: ValidationType = ValidationType.NotBlank,
val additionalText: String? = null
val additionalText: String? = null,
val provider: InvalidPasswordProvider? = null
) {
var cardType: CardType? = null
@ -87,6 +92,10 @@ data class InputValidationResult(
ValidationType.Password -> validatePassword()
ValidationType.PasswordMinLength -> validatePasswordMinLength(validationType.minLong)
ValidationType.PasswordMatch -> validateNotBlank() && text == additionalText
ValidationType.InvalidPassword -> when {
provider == null -> true
else -> !provider.isPasswordCommon(text)
}
ValidationType.Email -> validateEmail()
ValidationType.CreditCard -> {
cardType = validateCreditCard()

View File

@ -0,0 +1,66 @@
package me.proton.core.presentation.utils
import android.content.Context
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import java.io.ByteArrayInputStream
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class InvalidPasswordProviderTest {
@MockK
private lateinit var context: Context
private lateinit var tested: InvalidPasswordProvider
@BeforeTest
fun setUp() {
MockKAnnotations.init(this)
tested = InvalidPasswordProvider(context)
}
@Test
fun `empty passwords file`() = runTest {
mockCommonPasswordAssets("")
tested.init()
assertFalse(tested.isPasswordCommon("password"))
}
@Test
fun `passwords not initialized`() = runTest {
mockCommonPasswordAssets("password")
// Call to `tested.init()` skipped
assertFalse(tested.isPasswordCommon("password"))
}
@Test
fun `single password`() = runTest {
mockCommonPasswordAssets("password")
tested.init()
assertTrue(tested.isPasswordCommon("password"))
}
@Test
fun `multiple passwords`() = runTest {
mockCommonPasswordAssets("password\n\n \nqwerty\n asdf\n")
tested.init()
assertTrue(tested.isPasswordCommon("password"))
assertTrue(tested.isPasswordCommon("qwerty"))
assertTrue(tested.isPasswordCommon("asdf"))
}
private fun mockCommonPasswordAssets(contents: String) {
every { context.assets } returns mockk {
every { open(any()) } returns ByteArrayInputStream(contents.toByteArray())
}
}
}

View File

@ -47,6 +47,7 @@ dependencies {
project(Module.eventManagerDomain),
project(Module.networkData),
project(Module.pushDomain),
project(Module.kotlinUtil),
`coroutines-core`,
`javax-inject`,
)
@ -54,7 +55,6 @@ dependencies {
implementation(
project(Module.userData),
project(Module.data),
project(Module.kotlinUtil),
project(Module.networkDomain),
retrofit,
`room-ktx`,
@ -70,6 +70,7 @@ dependencies {
project(Module.cryptoCommon),
project(Module.keyDomain),
project(Module.kotlinTest),
project(Module.userDomain),
`android-test-core`,
`android-work-testing`,
`coroutines-test`,

View File

@ -19,41 +19,33 @@
package me.proton.core.push.data.remote.worker
import android.content.Context
import androidx.hilt.work.HiltWorkerFactory
import androidx.test.core.app.ApplicationProvider
import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import androidx.work.testing.TestListenableWorkerBuilder
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import io.mockk.MockKAnnotations
import io.mockk.MockKStubScope
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import me.proton.core.domain.entity.UserId
import me.proton.core.eventmanager.domain.EventManager
import me.proton.core.eventmanager.domain.EventManagerProvider
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.ApiResult
import me.proton.core.push.domain.entity.PushId
import me.proton.core.push.domain.entity.PushObjectType
import me.proton.core.push.domain.remote.PushRemoteDataSource
import me.proton.core.push.domain.repository.PushRepository
import me.proton.core.push.domain.usecase.DeletePushRemote
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import javax.inject.Inject
import kotlin.test.Test
import kotlin.test.assertFailsWith
@HiltAndroidTest
@Config(application = HiltTestApplication::class)
@RunWith(RobolectricTestRunner::class)
internal class DeletePushWorkerTest {
@ -63,23 +55,15 @@ internal class DeletePushWorkerTest {
private val testUserId = UserId("test-user-id")
private val testPushType = PushObjectType.Messages
@get:Rule
val hiltRule = HiltAndroidRule(this)
@MockK
private lateinit var pushRepository: PushRepository
@BindValue
@JvmField
internal val pushRepository: PushRepository = mockk()
@BindValue
@JvmField
internal val deletePushRemote: DeletePushRemote = mockk()
@Inject
internal lateinit var hiltWorkerFactory: HiltWorkerFactory
@MockK
private lateinit var deletePushRemote: DeletePushRemote
@Before
fun setUp() {
hiltRule.inject()
MockKAnnotations.init(this)
context = ApplicationProvider.getApplicationContext()
}
@ -106,7 +90,7 @@ internal class DeletePushWorkerTest {
@Test
fun `missing input data`() {
val worker = TestListenableWorkerBuilder<DeletePushWorker>(context, Data.EMPTY)
.setWorkerFactory(hiltWorkerFactory)
.setWorkerFactory(makeWorkerFactory())
.build()
assertFailsWith<IllegalArgumentException> {
runBlocking { worker.doWork() }
@ -121,8 +105,16 @@ internal class DeletePushWorkerTest {
val inputData = DeletePushWorker.makeInputData(testUserId, testPushId, testPushType.value)
val worker = TestListenableWorkerBuilder<DeletePushWorker>(context, inputData)
.setWorkerFactory(hiltWorkerFactory)
.setWorkerFactory(makeWorkerFactory())
.build()
return runBlocking { worker.doWork() }
}
private fun makeWorkerFactory() = object : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker = DeletePushWorker(appContext, workerParameters, pushRepository, deletePushRemote)
}
}