mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
PM-10118: Remember generator types (#3708)
This commit is contained in:
parent
5a0b1caecd
commit
4dbcec85bb
5 changed files with 148 additions and 23 deletions
|
@ -1,11 +1,14 @@
|
|||
package com.x8bit.bitwarden.data.tools.generator.repository.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A data class representing the configuration options for both password and passphrase generation.
|
||||
*
|
||||
* @property type The type of passcode to be generated, as defined in PasscodeType.
|
||||
* @property length The total length of the generated password.
|
||||
* @property allowAmbiguousChar Indicates whether ambiguous characters are allowed in the password.
|
||||
* @property hasNumbers Indicates whether the password should contain numbers.
|
||||
|
@ -23,6 +26,8 @@ import kotlinx.serialization.Serializable
|
|||
*/
|
||||
@Serializable
|
||||
data class PasscodeGenerationOptions(
|
||||
@SerialName("type")
|
||||
val type: PasscodeType,
|
||||
|
||||
// Password-specific options
|
||||
|
||||
|
@ -69,4 +74,22 @@ data class PasscodeGenerationOptions(
|
|||
|
||||
@SerialName("includeNumber")
|
||||
val allowIncludeNumber: Boolean,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Represents different Passcode types.
|
||||
*/
|
||||
@Serializable(with = PasscodeTypeSerializer::class)
|
||||
enum class PasscodeType {
|
||||
@SerialName("0")
|
||||
PASSWORD,
|
||||
|
||||
@SerialName("1")
|
||||
PASSPHRASE,
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class PasscodeTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<PasscodeGenerationOptions.PasscodeType>(
|
||||
PasscodeGenerationOptions.PasscodeType.entries.toTypedArray(),
|
||||
)
|
||||
|
|
|
@ -85,8 +85,16 @@ class GeneratorViewModel @Inject constructor(
|
|||
GeneratorState(
|
||||
generatedText = NO_GENERATED_TEXT,
|
||||
selectedType = when (generatorMode) {
|
||||
is GeneratorMode.Modal.Username -> Username()
|
||||
GeneratorMode.Modal.Password -> Passcode()
|
||||
is GeneratorMode.Modal.Username -> {
|
||||
val type = generatorRepository.getUsernameGenerationOptions().usernameType
|
||||
Username(selectedType = type)
|
||||
}
|
||||
|
||||
GeneratorMode.Modal.Password -> {
|
||||
val type = generatorRepository.getPasscodeGenerationOptions().passcodeType
|
||||
Passcode(selectedType = type)
|
||||
}
|
||||
|
||||
GeneratorMode.Default -> Passcode(selectedType = Password())
|
||||
},
|
||||
generatorMode = generatorMode,
|
||||
|
@ -260,7 +268,10 @@ class GeneratorViewModel @Inject constructor(
|
|||
|
||||
private fun loadOptions() {
|
||||
when (val selectedType = state.selectedType) {
|
||||
is Passcode -> loadPasscodeOptions(selectedType = selectedType, usePolicyDefault = true)
|
||||
is Passcode -> loadPasscodeOptions(
|
||||
selectedType = selectedType,
|
||||
)
|
||||
|
||||
is Username -> loadUsernameOptions(
|
||||
selectedType = selectedType,
|
||||
forceRegeneration = selectedType.selectedType !is ForwardedEmailAlias,
|
||||
|
@ -269,26 +280,14 @@ class GeneratorViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun loadPasscodeOptions(selectedType: Passcode, usePolicyDefault: Boolean) {
|
||||
val passwordType = if (usePolicyDefault) {
|
||||
Passcode(
|
||||
selectedType = generatorRepository
|
||||
.getPasswordGeneratorPolicy()
|
||||
?.defaultType
|
||||
?.toSelectedType()
|
||||
?: Password(),
|
||||
)
|
||||
} else {
|
||||
selectedType
|
||||
}
|
||||
|
||||
private fun loadPasscodeOptions(selectedType: Passcode) {
|
||||
val options = generatorRepository.getPasscodeGenerationOptions()
|
||||
?: generatePasscodeDefaultOptions()
|
||||
|
||||
val policy = policyManager
|
||||
.getActivePolicies<PolicyInformation.PasswordGenerator>()
|
||||
.toStrictestPolicy()
|
||||
when (passwordType.selectedType) {
|
||||
when (selectedType.selectedType) {
|
||||
is Passphrase -> {
|
||||
val minNumWords = policy.minNumberWords ?: Passphrase.PASSPHRASE_MIN_NUMBER_OF_WORDS
|
||||
val passphrase = Passphrase(
|
||||
|
@ -380,6 +379,7 @@ class GeneratorViewModel @Inject constructor(
|
|||
val options = generatorRepository
|
||||
.getPasscodeGenerationOptions() ?: generatePasscodeDefaultOptions()
|
||||
val newOptions = options.copy(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = password.length,
|
||||
allowAmbiguousChar = password.avoidAmbiguousChars,
|
||||
hasNumbers = password.useNumbers,
|
||||
|
@ -396,6 +396,7 @@ class GeneratorViewModel @Inject constructor(
|
|||
val options = generatorRepository
|
||||
.getPasscodeGenerationOptions() ?: generatePasscodeDefaultOptions()
|
||||
val newOptions = options.copy(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSPHRASE,
|
||||
numWords = passphrase.numWords,
|
||||
wordSeparator = passphrase.wordSeparator.toString(),
|
||||
allowCapitalize = passphrase.capitalize,
|
||||
|
@ -485,6 +486,7 @@ class GeneratorViewModel @Inject constructor(
|
|||
val defaultPassphrase = Passphrase()
|
||||
|
||||
return PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = defaultPassword.length,
|
||||
allowAmbiguousChar = defaultPassword.avoidAmbiguousChars,
|
||||
hasNumbers = defaultPassword.useNumbers,
|
||||
|
@ -684,11 +686,18 @@ class GeneratorViewModel @Inject constructor(
|
|||
private fun handleMainTypeOptionSelect(action: GeneratorAction.MainTypeOptionSelect) {
|
||||
when (action.mainTypeOption) {
|
||||
GeneratorState.MainTypeOption.PASSWORD -> {
|
||||
loadPasscodeOptions(selectedType = Passcode(), usePolicyDefault = true)
|
||||
val type = generatorRepository.getPasscodeGenerationOptions().passcodeType
|
||||
loadPasscodeOptions(
|
||||
selectedType = Passcode(selectedType = type),
|
||||
)
|
||||
}
|
||||
|
||||
GeneratorState.MainTypeOption.USERNAME -> {
|
||||
loadUsernameOptions(selectedType = Username(), forceRegeneration = true)
|
||||
val type = generatorRepository.getUsernameGenerationOptions().usernameType
|
||||
loadUsernameOptions(
|
||||
selectedType = Username(selectedType = type),
|
||||
forceRegeneration = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -703,12 +712,10 @@ class GeneratorViewModel @Inject constructor(
|
|||
when (action.passcodeTypeOption) {
|
||||
PasscodeTypeOption.PASSWORD -> loadPasscodeOptions(
|
||||
selectedType = Passcode(selectedType = Password()),
|
||||
usePolicyDefault = false,
|
||||
)
|
||||
|
||||
PasscodeTypeOption.PASSPHRASE -> loadPasscodeOptions(
|
||||
selectedType = Passcode(selectedType = Passphrase()),
|
||||
usePolicyDefault = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2572,6 +2579,22 @@ private val Password.minimumLength: Int
|
|||
return minLowercase + minUppercase + minimumNumbers + minimumSpecial
|
||||
}
|
||||
|
||||
private val PasscodeGenerationOptions?.passcodeType: Passcode.PasscodeType
|
||||
get() = when (this?.type) {
|
||||
PasscodeGenerationOptions.PasscodeType.PASSWORD -> Password()
|
||||
PasscodeGenerationOptions.PasscodeType.PASSPHRASE -> Passphrase()
|
||||
else -> Password()
|
||||
}
|
||||
|
||||
private val UsernameGenerationOptions?.usernameType: Username.UsernameType
|
||||
get() = when (this?.type) {
|
||||
UsernameGenerationOptions.UsernameType.PLUS_ADDRESSED_EMAIL -> PlusAddressedEmail()
|
||||
UsernameGenerationOptions.UsernameType.CATCH_ALL_EMAIL -> CatchAllEmail()
|
||||
UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS -> ForwardedEmailAlias()
|
||||
UsernameGenerationOptions.UsernameType.RANDOM_WORD -> RandomWord()
|
||||
else -> PlusAddressedEmail()
|
||||
}
|
||||
|
||||
private fun UsernameGenerationOptions.ForwardedEmailServiceType?.toServiceType(
|
||||
options: UsernameGenerationOptions,
|
||||
): ForwardedEmailAlias.ServiceType? {
|
||||
|
|
|
@ -29,6 +29,7 @@ class GeneratorDiskSourceTest {
|
|||
val userId = "userId"
|
||||
|
||||
val passcodeOptions = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
@ -81,6 +82,7 @@ class GeneratorDiskSourceTest {
|
|||
fun `getPasscodeGenerationOptions should return correct options when available`() {
|
||||
val userId = "user123"
|
||||
val options = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
@ -118,6 +120,7 @@ class GeneratorDiskSourceTest {
|
|||
fun `storePasscodeGenerationOptions should correctly store options`() {
|
||||
val userId = "user123"
|
||||
val options = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
|
|
@ -421,6 +421,7 @@ class GeneratorRepositoryTest {
|
|||
fun `getPasscodeGenerationOptions should return options when available`() = runTest {
|
||||
val userId = "activeUserId"
|
||||
val expectedOptions = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
@ -476,6 +477,7 @@ class GeneratorRepositoryTest {
|
|||
fun `savePasscodeGenerationOptions should store options correctly`() = runTest {
|
||||
val userId = "activeUserId"
|
||||
val optionsToSave = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
@ -607,6 +609,7 @@ class GeneratorRepositoryTest {
|
|||
fun `savePasscodeGenerationOptions should not store options when there is no active user`() =
|
||||
runTest {
|
||||
val optionsToSave = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswo
|
|||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
|
@ -112,6 +113,70 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(initialPasscodeState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct for username modal`() {
|
||||
val usernameGenerationOptions = UsernameGenerationOptions(
|
||||
type = UsernameGenerationOptions.UsernameType.RANDOM_WORD,
|
||||
)
|
||||
fakeGeneratorRepository.saveUsernameGenerationOptions(usernameGenerationOptions)
|
||||
val expected = GeneratorState(
|
||||
generatedText = "randomWord",
|
||||
selectedType = GeneratorState.MainType.Username(
|
||||
selectedType = GeneratorState.MainType.Username.UsernameType.RandomWord(
|
||||
capitalize = false,
|
||||
includeNumber = false,
|
||||
),
|
||||
),
|
||||
generatorMode = GeneratorMode.Modal.Username(website = ""),
|
||||
currentEmailAddress = "currentEmail",
|
||||
isUnderPolicy = false,
|
||||
website = "",
|
||||
)
|
||||
|
||||
val viewModel = createViewModel(
|
||||
state = null,
|
||||
type = "username_generator",
|
||||
website = "",
|
||||
)
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct for passcode modal`() {
|
||||
val passcodeGenerationOptions = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSPHRASE,
|
||||
length = 14,
|
||||
allowAmbiguousChar = true,
|
||||
hasNumbers = true,
|
||||
minNumber = 1,
|
||||
hasUppercase = true,
|
||||
hasLowercase = true,
|
||||
allowSpecial = false,
|
||||
minSpecial = 1,
|
||||
numWords = 3,
|
||||
wordSeparator = "-",
|
||||
allowCapitalize = false,
|
||||
allowIncludeNumber = false,
|
||||
)
|
||||
fakeGeneratorRepository.savePasscodeGenerationOptions(passcodeGenerationOptions)
|
||||
val expected = GeneratorState(
|
||||
generatedText = "updatedPassphrase",
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(),
|
||||
),
|
||||
generatorMode = GeneratorMode.Modal.Password,
|
||||
currentEmailAddress = "currentEmail",
|
||||
isUnderPolicy = false,
|
||||
website = null,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel(
|
||||
state = null,
|
||||
type = "password_generator",
|
||||
)
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `activePolicyFlow changes should update state`() = runTest {
|
||||
val payload = mapOf(
|
||||
|
@ -253,6 +318,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
val initialState = viewModel.stateFlow.value
|
||||
|
||||
val updatedPasswordOptions = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSWORD,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
@ -331,6 +397,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
val initialState = viewModel.stateFlow.value
|
||||
|
||||
val updatedPassphraseOptions = PasscodeGenerationOptions(
|
||||
type = PasscodeGenerationOptions.PasscodeType.PASSPHRASE,
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
|
@ -2336,8 +2403,14 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private fun createViewModel(
|
||||
state: GeneratorState? = initialPasscodeState,
|
||||
type: String? = null,
|
||||
website: String? = null,
|
||||
): GeneratorViewModel = createViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
set("generator_mode_type", type)
|
||||
set("generator_website", website)
|
||||
},
|
||||
)
|
||||
|
||||
private fun setupMockPassphraseTypePolicy() {
|
||||
|
|
Loading…
Add table
Reference in a new issue