mirror of
https://github.com/bitwarden/android.git
synced 2024-11-26 19:36:18 +03:00
BIT-653: Adding passphrase generation implementation (#277)
This commit is contained in:
parent
baafab6e67
commit
636600114b
2 changed files with 162 additions and 30 deletions
|
@ -5,9 +5,11 @@ package com.x8bit.bitwarden.ui.tools.feature.generator
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.PassphraseGeneratorRequest
|
||||
import com.bitwarden.core.PasswordGeneratorRequest
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -89,6 +91,10 @@ class GeneratorViewModel @Inject constructor(
|
|||
is GeneratorAction.Internal.UpdateGeneratedPasswordResult -> {
|
||||
handleUpdateGeneratedPasswordResult(action)
|
||||
}
|
||||
|
||||
is GeneratorAction.Internal.UpdateGeneratedPassphraseResult -> {
|
||||
handleUpdateGeneratedPassphraseResult(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,28 +103,33 @@ class GeneratorViewModel @Inject constructor(
|
|||
//region Generation Handlers
|
||||
|
||||
private fun loadPasscodeOptions(selectedType: Passcode) {
|
||||
val options = generatorRepository.getPasscodeGenerationOptions()
|
||||
?: generatePasscodeDefaultOptions()
|
||||
|
||||
when (selectedType.selectedType) {
|
||||
is Passphrase -> {
|
||||
mutableStateFlow.update { it.copy(selectedType = selectedType) }
|
||||
// TODO: App should generate passphrases (BIT-653)
|
||||
val passphrase = Passphrase(
|
||||
numWords = options.numWords,
|
||||
wordSeparator = options.wordSeparator.toCharArray().first(),
|
||||
capitalize = options.allowCapitalize,
|
||||
includeNumber = options.allowIncludeNumber,
|
||||
)
|
||||
updateGeneratorMainType {
|
||||
Passcode(selectedType = passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
is Password -> {
|
||||
val options = generatorRepository.getPasscodeGenerationOptions()
|
||||
val password = if (options != null) {
|
||||
Password(
|
||||
length = options.length,
|
||||
useCapitals = options.hasUppercase,
|
||||
useLowercase = options.hasLowercase,
|
||||
useNumbers = options.hasNumbers,
|
||||
useSpecialChars = options.allowSpecial,
|
||||
minNumbers = options.minNumber,
|
||||
minSpecial = options.minSpecial,
|
||||
avoidAmbiguousChars = options.allowAmbiguousChar,
|
||||
)
|
||||
} else {
|
||||
Password()
|
||||
}
|
||||
val password = Password(
|
||||
length = options.length,
|
||||
useCapitals = options.hasUppercase,
|
||||
useLowercase = options.hasLowercase,
|
||||
useNumbers = options.hasNumbers,
|
||||
useSpecialChars = options.allowSpecial,
|
||||
minNumbers = options.minNumber,
|
||||
minSpecial = options.minSpecial,
|
||||
avoidAmbiguousChars = options.allowAmbiguousChar,
|
||||
)
|
||||
updateGeneratorMainType {
|
||||
Passcode(selectedType = password)
|
||||
}
|
||||
|
@ -149,6 +160,18 @@ class GeneratorViewModel @Inject constructor(
|
|||
generatorRepository.savePasscodeGenerationOptions(newOptions)
|
||||
}
|
||||
|
||||
private fun savePassphraseOptionsToDisk(passphrase: Passphrase) {
|
||||
val options = generatorRepository
|
||||
.getPasscodeGenerationOptions() ?: generatePasscodeDefaultOptions()
|
||||
val newOptions = options.copy(
|
||||
numWords = passphrase.numWords,
|
||||
wordSeparator = passphrase.wordSeparator.toString(),
|
||||
allowCapitalize = passphrase.capitalize,
|
||||
allowIncludeNumber = passphrase.includeNumber,
|
||||
)
|
||||
generatorRepository.savePasscodeGenerationOptions(newOptions)
|
||||
}
|
||||
|
||||
private fun generatePasscodeDefaultOptions(): PasscodeGenerationOptions {
|
||||
val defaultPassword = Password()
|
||||
val defaultPassphrase = Passphrase()
|
||||
|
@ -187,6 +210,18 @@ class GeneratorViewModel @Inject constructor(
|
|||
sendAction(GeneratorAction.Internal.UpdateGeneratedPasswordResult(result))
|
||||
}
|
||||
|
||||
private suspend fun generatePassphrase(passphrase: Passphrase) {
|
||||
val request = PassphraseGeneratorRequest(
|
||||
numWords = passphrase.numWords.toUByte(),
|
||||
wordSeparator = passphrase.wordSeparator.toString(),
|
||||
capitalize = passphrase.capitalize,
|
||||
includeNumber = passphrase.includeNumber,
|
||||
)
|
||||
|
||||
val result = generatorRepository.generatePassphrase(request)
|
||||
sendAction(GeneratorAction.Internal.UpdateGeneratedPassphraseResult(result))
|
||||
}
|
||||
|
||||
//endregion Generation Handlers
|
||||
|
||||
//region Generated Field Handlers
|
||||
|
@ -217,6 +252,22 @@ class GeneratorViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateGeneratedPassphraseResult(
|
||||
action: GeneratorAction.Internal.UpdateGeneratedPassphraseResult,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is GeneratedPassphraseResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(generatedText = result.generatedString)
|
||||
}
|
||||
}
|
||||
|
||||
GeneratedPassphraseResult.InvalidRequest -> {
|
||||
sendEvent(GeneratorEvent.ShowSnackbar(R.string.an_error_has_occurred.asText()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Generated Field Handlers
|
||||
|
||||
//region Main Type Option Handlers
|
||||
|
@ -455,7 +506,8 @@ class GeneratorViewModel @Inject constructor(
|
|||
when (updatedMainType) {
|
||||
is Passcode -> when (val selectedType = updatedMainType.selectedType) {
|
||||
is Passphrase -> {
|
||||
// TODO: App should generate passphrases (BIT-653)
|
||||
savePassphraseOptionsToDisk(selectedType)
|
||||
generatePassphrase(selectedType)
|
||||
}
|
||||
|
||||
is Password -> {
|
||||
|
@ -1068,6 +1120,13 @@ sealed class GeneratorAction {
|
|||
data class UpdateGeneratedPasswordResult(
|
||||
val result: GeneratedPasswordResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a generated text update is received.
|
||||
*/
|
||||
data class UpdateGeneratedPassphraseResult(
|
||||
val result: GeneratedPassphraseResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.generator
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||
|
@ -100,18 +101,66 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `RegenerateClick for passphrase state should do nothing`() = runTest {
|
||||
val viewModel = GeneratorViewModel(passphraseSavedStateHandle, fakeGeneratorRepository)
|
||||
fun `RegenerateClick action for passphrase state updates generatedText and saves passphrase generation options on successful passphrase generation`() =
|
||||
runTest {
|
||||
val updatedGeneratedPassphrase = "updatedPassphrase"
|
||||
|
||||
fakeGeneratorRepository.setMockGeneratePasswordResult(
|
||||
GeneratedPasswordResult.Success("DifferentPassphrase"),
|
||||
)
|
||||
val viewModel = createViewModel(initialPassphraseState)
|
||||
val initialState = viewModel.stateFlow.value
|
||||
|
||||
viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick)
|
||||
val updatedPassphraseOptions = PasscodeGenerationOptions(
|
||||
length = 14,
|
||||
allowAmbiguousChar = false,
|
||||
hasNumbers = true,
|
||||
minNumber = 1,
|
||||
hasUppercase = true,
|
||||
minUppercase = null,
|
||||
hasLowercase = true,
|
||||
minLowercase = null,
|
||||
allowSpecial = false,
|
||||
minSpecial = 1,
|
||||
allowCapitalize = false,
|
||||
allowIncludeNumber = false,
|
||||
wordSeparator = "-",
|
||||
numWords = 3,
|
||||
)
|
||||
|
||||
assertEquals(initialPassphraseState, viewModel.stateFlow.value)
|
||||
}
|
||||
fakeGeneratorRepository.setMockGeneratePassphraseResult(
|
||||
GeneratedPassphraseResult.Success(updatedGeneratedPassphrase),
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick)
|
||||
|
||||
val expectedState = initialState.copy(generatedText = updatedGeneratedPassphrase)
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
|
||||
assertEquals(
|
||||
updatedPassphraseOptions,
|
||||
fakeGeneratorRepository.getPasscodeGenerationOptions(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `RegenerateClick action for passphrase state sends ShowSnackbar event on passphrase generation failure`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel(initialPassphraseState)
|
||||
|
||||
fakeGeneratorRepository.setMockGeneratePassphraseResult(
|
||||
GeneratedPassphraseResult.InvalidRequest,
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
GeneratorEvent.ShowSnackbar(R.string.an_error_has_occurred.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RegenerateClick for username state should do nothing`() = runTest {
|
||||
|
@ -200,8 +249,10 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `PasscodeTypeOptionSelect PASSPHRASE should switch to PassphraseType`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val updatedText = "updatedPassphrase"
|
||||
|
||||
fakeGeneratorRepository.setMockGeneratePasswordResult(
|
||||
GeneratedPasswordResult.Success("updatedText"),
|
||||
GeneratedPasswordResult.Success(updatedText),
|
||||
)
|
||||
|
||||
val action = GeneratorAction.MainType.Passcode.PasscodeTypeOptionSelect(
|
||||
|
@ -211,6 +262,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedState = initialState.copy(
|
||||
generatedText = updatedText,
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(),
|
||||
),
|
||||
|
@ -492,9 +544,6 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
inner class PassphraseActions {
|
||||
|
||||
private val defaultPassphraseState = createPassphraseState()
|
||||
private val passphraseSavedStateHandle =
|
||||
createSavedStateHandleWithState(defaultPassphraseState)
|
||||
|
||||
private lateinit var viewModel: GeneratorViewModel
|
||||
|
||||
@BeforeEach
|
||||
|
@ -508,6 +557,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `NumWordsCounterChange should update the numWords property correctly`() =
|
||||
runTest {
|
||||
val updatedGeneratedPassphrase = "updatedPassword"
|
||||
fakeGeneratorRepository.setMockGeneratePassphraseResult(
|
||||
GeneratedPassphraseResult.Success(updatedGeneratedPassphrase),
|
||||
)
|
||||
|
||||
val newNumWords = 4
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction
|
||||
|
@ -521,6 +575,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
val expectedState = defaultPassphraseState.copy(
|
||||
generatedText = updatedGeneratedPassphrase,
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
|
||||
numWords = newNumWords,
|
||||
|
@ -534,6 +589,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `WordSeparatorTextChange should update wordSeparator correctly to new value`() =
|
||||
runTest {
|
||||
val updatedGeneratedPassphrase = "updatedPassword"
|
||||
fakeGeneratorRepository.setMockGeneratePassphraseResult(
|
||||
GeneratedPassphraseResult.Success(updatedGeneratedPassphrase),
|
||||
)
|
||||
|
||||
val newWordSeparatorChar = '_'
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
|
@ -547,6 +607,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
val expectedState = defaultPassphraseState.copy(
|
||||
generatedText = updatedGeneratedPassphrase,
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
|
||||
wordSeparator = newWordSeparatorChar,
|
||||
|
@ -560,6 +621,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `ToggleIncludeNumberChange should update the includeNumber property correctly`() =
|
||||
runTest {
|
||||
val updatedGeneratedPassphrase = "updatedPassword"
|
||||
fakeGeneratorRepository.setMockGeneratePassphraseResult(
|
||||
GeneratedPassphraseResult.Success(updatedGeneratedPassphrase),
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction
|
||||
.MainType
|
||||
|
@ -572,6 +638,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
val expectedState = defaultPassphraseState.copy(
|
||||
generatedText = updatedGeneratedPassphrase,
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
selectedType = GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
|
||||
includeNumber = true,
|
||||
|
@ -585,6 +652,11 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `ToggleCapitalizeChange should update the capitalize property correctly`() =
|
||||
runTest {
|
||||
val updatedGeneratedPassphrase = "updatedPassword"
|
||||
fakeGeneratorRepository.setMockGeneratePassphraseResult(
|
||||
GeneratedPassphraseResult.Success(updatedGeneratedPassphrase),
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(
|
||||
GeneratorAction
|
||||
.MainType
|
||||
|
@ -597,6 +669,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
val expectedState = defaultPassphraseState.copy(
|
||||
generatedText = updatedGeneratedPassphrase,
|
||||
selectedType = GeneratorState.MainType.Passcode(
|
||||
GeneratorState.MainType.Passcode.PasscodeType.Passphrase(
|
||||
capitalize = true,
|
||||
|
|
Loading…
Reference in a new issue