PM-10118: Remember generator types (#3708)

This commit is contained in:
Shannon Draeker 2024-08-13 09:27:54 -04:00 committed by GitHub
parent 5a0b1caecd
commit 4dbcec85bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 23 deletions

View file

@ -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(),
)

View file

@ -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? {

View file

@ -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,

View file

@ -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,

View file

@ -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() {