BIT-1524, BIT-898: Update generated text (#964)

This commit is contained in:
David Perez 2024-02-07 14:31:52 -06:00 committed by Álison Fernandes
parent 6294e656ce
commit dc2e07c130
5 changed files with 213 additions and 57 deletions

View file

@ -18,6 +18,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@ -33,6 +34,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -470,7 +472,7 @@ private fun PasscodeOptionsItem(
currentSubState: GeneratorState.MainType.Passcode, currentSubState: GeneratorState.MainType.Passcode,
onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit, onSubStateOptionClicked: (GeneratorState.MainType.Passcode.PasscodeTypeOption) -> Unit,
) { ) {
val possibleSubStates = GeneratorState.MainType.Passcode.PasscodeTypeOption.values().toList() val possibleSubStates = GeneratorState.MainType.Passcode.PasscodeTypeOption.entries
val optionsWithStrings = possibleSubStates.associateWith { stringResource(id = it.labelRes) } val optionsWithStrings = possibleSubStates.associateWith { stringResource(id = it.labelRes) }
BitwardenMultiSelectButton( BitwardenMultiSelectButton(
@ -622,6 +624,12 @@ private fun PasswordLengthSliderItem(
}, },
valueRange = sliderRange, valueRange = sliderRange,
steps = maxValue - 1, steps = maxValue - 1,
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
disabledActiveTickColor = Color.Transparent,
disabledInactiveTickColor = Color.Transparent,
),
modifier = Modifier modifier = Modifier
.semantics { testTag = "PasswordLengthSlider" } .semantics { testTag = "PasswordLengthSlider" }
.weight(1f), .weight(1f),
@ -782,8 +790,7 @@ private fun ColumnScope.PassphraseTypeContent(
PassphraseWordSeparatorInputItem( PassphraseWordSeparatorInputItem(
wordSeparator = passphraseTypeState.wordSeparator, wordSeparator = passphraseTypeState.wordSeparator,
onPassphraseWordSeparatorChange = onPassphraseWordSeparatorChange = passphraseHandlers.onPassphraseWordSeparatorChange,
passphraseHandlers.onPassphraseWordSeparatorChange,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -835,13 +842,19 @@ private fun PassphraseWordSeparatorInputItem(
) { ) {
BitwardenTextField( BitwardenTextField(
label = stringResource(id = R.string.word_separator), label = stringResource(id = R.string.word_separator),
value = wordSeparator?.toString() ?: "", value = wordSeparator?.toString().orEmpty(),
onValueChange = { onValueChange = {
onPassphraseWordSeparatorChange(it.toCharArray().firstOrNull()) // When onValueChange triggers and we don't update the value for whatever reason,
// onValueChange triggers again with the previous value.
// To avoid passphrase regeneration, we filter out those re-emissions.
val char = it.firstOrNull()
if (char != wordSeparator) {
onPassphraseWordSeparatorChange(char)
}
}, },
modifier = Modifier modifier = Modifier
.semantics { testTag = "WordSeparatorEntry" } .semantics { testTag = "WordSeparatorEntry" }
.width(267.dp) .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) )
} }

View file

@ -57,6 +57,7 @@ import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
private const val NO_GENERATED_TEXT: String = "-"
/** /**
* ViewModel responsible for handling user interactions in the generator screen. * ViewModel responsible for handling user interactions in the generator screen.
@ -77,7 +78,7 @@ class GeneratorViewModel @Inject constructor(
private val policyManager: PolicyManager, private val policyManager: PolicyManager,
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>( ) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
initialState = savedStateHandle[KEY_STATE] ?: GeneratorState( initialState = savedStateHandle[KEY_STATE] ?: GeneratorState(
generatedText = "", generatedText = NO_GENERATED_TEXT,
selectedType = when (GeneratorArgs(savedStateHandle).type) { selectedType = when (GeneratorArgs(savedStateHandle).type) {
GeneratorMode.Modal.Username -> Username() GeneratorMode.Modal.Username -> Username()
GeneratorMode.Modal.Password -> Passcode() GeneratorMode.Modal.Password -> Passcode()
@ -309,7 +310,7 @@ class GeneratorViewModel @Inject constructor(
} }
} }
private fun loadUsernameOptions(selectedType: Username) { private fun loadUsernameOptions(selectedType: Username, forceRegeneration: Boolean = false) {
val options = generatorRepository.getUsernameGenerationOptions() val options = generatorRepository.getUsernameGenerationOptions()
val updatedSelectedType = when (val type = selectedType.selectedType) { val updatedSelectedType = when (val type = selectedType.selectedType) {
is PlusAddressedEmail -> { is PlusAddressedEmail -> {
@ -351,7 +352,7 @@ class GeneratorViewModel @Inject constructor(
} }
} }
mutableStateFlow.update { it.copy(selectedType = updatedSelectedType) } updateGeneratorMainType(forceRegeneration = forceRegeneration) { updatedSelectedType }
} }
private fun savePasswordOptionsToDisk(password: Password) { private fun savePasswordOptionsToDisk(password: Password) {
@ -520,7 +521,7 @@ class GeneratorViewModel @Inject constructor(
private suspend fun generatePassphrase(passphrase: Passphrase) { private suspend fun generatePassphrase(passphrase: Passphrase) {
val request = PassphraseGeneratorRequest( val request = PassphraseGeneratorRequest(
numWords = passphrase.numWords.toUByte(), numWords = passphrase.numWords.toUByte(),
wordSeparator = passphrase.wordSeparator.toString(), wordSeparator = passphrase.wordSeparator?.toString() ?: " ",
capitalize = passphrase.capitalize, capitalize = passphrase.capitalize,
includeNumber = passphrase.includeNumber, includeNumber = passphrase.includeNumber,
) )
@ -536,7 +537,7 @@ class GeneratorViewModel @Inject constructor(
private fun handleRegenerationClick() { private fun handleRegenerationClick() {
// Go through the update process with the current state to trigger a // Go through the update process with the current state to trigger a
// regeneration of the generated text for the same state. // regeneration of the generated text for the same state.
updateGeneratorMainType(isManualRegeneration = true) { mutableStateFlow.value.selectedType } updateGeneratorMainType(forceRegeneration = true) { mutableStateFlow.value.selectedType }
} }
private fun handleCopyClick() { private fun handleCopyClick() {
@ -650,10 +651,12 @@ class GeneratorViewModel @Inject constructor(
private fun handleMainTypeOptionSelect(action: GeneratorAction.MainTypeOptionSelect) { private fun handleMainTypeOptionSelect(action: GeneratorAction.MainTypeOptionSelect) {
when (action.mainTypeOption) { when (action.mainTypeOption) {
GeneratorState.MainTypeOption.PASSWORD -> { GeneratorState.MainTypeOption.PASSWORD -> {
loadPasscodeOptions(Passcode(), usePolicyDefault = true) loadPasscodeOptions(selectedType = Passcode(), usePolicyDefault = true)
} }
GeneratorState.MainTypeOption.USERNAME -> loadUsernameOptions(Username()) GeneratorState.MainTypeOption.USERNAME -> {
loadUsernameOptions(selectedType = Username(), forceRegeneration = true)
}
} }
} }
@ -883,18 +886,24 @@ class GeneratorViewModel @Inject constructor(
when (action.usernameTypeOption) { when (action.usernameTypeOption) {
Username.UsernameTypeOption.PLUS_ADDRESSED_EMAIL -> loadUsernameOptions( Username.UsernameTypeOption.PLUS_ADDRESSED_EMAIL -> loadUsernameOptions(
selectedType = Username(selectedType = PlusAddressedEmail()), selectedType = Username(selectedType = PlusAddressedEmail()),
forceRegeneration = true,
) )
Username.UsernameTypeOption.CATCH_ALL_EMAIL -> loadUsernameOptions( Username.UsernameTypeOption.CATCH_ALL_EMAIL -> loadUsernameOptions(
selectedType = Username(selectedType = CatchAllEmail()), selectedType = Username(selectedType = CatchAllEmail()),
forceRegeneration = true,
) )
// We do not force regeneration here since the API can fail if the data is entered
// incorrectly. This will only be generated when the user clicks the regenerate button.
Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> loadUsernameOptions( Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> loadUsernameOptions(
selectedType = Username(selectedType = ForwardedEmailAlias()), selectedType = Username(selectedType = ForwardedEmailAlias()),
forceRegeneration = false,
) )
Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions( Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions(
selectedType = Username(selectedType = RandomWord()), selectedType = Username(selectedType = RandomWord()),
forceRegeneration = true,
) )
} }
} }
@ -1170,7 +1179,7 @@ class GeneratorViewModel @Inject constructor(
//region Utility Functions //region Utility Functions
private inline fun updateGeneratorMainType( private inline fun updateGeneratorMainType(
isManualRegeneration: Boolean = false, forceRegeneration: Boolean = false,
crossinline block: (GeneratorState.MainType) -> GeneratorState.MainType?, crossinline block: (GeneratorState.MainType) -> GeneratorState.MainType?,
) { ) {
val currentSelectedType = mutableStateFlow.value.selectedType val currentSelectedType = mutableStateFlow.value.selectedType
@ -1195,30 +1204,34 @@ class GeneratorViewModel @Inject constructor(
is Username -> when (val selectedType = updatedMainType.selectedType) { is Username -> when (val selectedType = updatedMainType.selectedType) {
is ForwardedEmailAlias -> { is ForwardedEmailAlias -> {
saveForwardedEmailAliasServiceTypeToDisk(selectedType) saveForwardedEmailAliasServiceTypeToDisk(selectedType)
if (isManualRegeneration) { if (forceRegeneration) {
generateForwardedEmailAlias(selectedType) generateForwardedEmailAlias(selectedType)
} else {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
} }
} }
is CatchAllEmail -> { is CatchAllEmail -> {
saveCatchAllEmailOptionsToDisk(selectedType) saveCatchAllEmailOptionsToDisk(selectedType)
if (isManualRegeneration) { if (forceRegeneration) {
generateCatchAllEmail(selectedType) generateCatchAllEmail(selectedType)
} else {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
} }
} }
is PlusAddressedEmail -> { is PlusAddressedEmail -> {
savePlusAddressedEmailOptionsToDisk(selectedType) savePlusAddressedEmailOptionsToDisk(selectedType)
if (isManualRegeneration) { if (forceRegeneration) {
generatePlusAddressedEmail(selectedType) generatePlusAddressedEmail(selectedType)
} else {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
} }
} }
is RandomWord -> { is RandomWord -> {
saveRandomWordOptionsToDisk(selectedType) saveRandomWordOptionsToDisk(selectedType)
if (isManualRegeneration) { generateRandomWordUsername(selectedType)
generateRandomWordUsername(selectedType)
}
} }
} }
} }
@ -1226,7 +1239,10 @@ class GeneratorViewModel @Inject constructor(
} }
private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) { private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) {
val request = alias.selectedServiceType?.toUsernameGeneratorRequest() ?: return val request = alias.selectedServiceType?.toUsernameGeneratorRequest() ?: run {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
return
}
val result = generatorRepository.generateForwardedServiceUsername(request) val result = generatorRepository.generateForwardedServiceUsername(request)
sendAction(GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult(result)) sendAction(GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult(result))
} }
@ -1242,10 +1258,14 @@ class GeneratorViewModel @Inject constructor(
} }
private suspend fun generateCatchAllEmail(catchAllEmail: CatchAllEmail) { private suspend fun generateCatchAllEmail(catchAllEmail: CatchAllEmail) {
val domainName = catchAllEmail.domainName.orNullIfBlank() ?: run {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
return
}
val result = generatorRepository.generateCatchAllEmail( val result = generatorRepository.generateCatchAllEmail(
UsernameGeneratorRequest.Catchall( UsernameGeneratorRequest.Catchall(
type = AppendType.Random, type = AppendType.Random,
domain = catchAllEmail.domainName, domain = domainName,
), ),
) )
sendAction(GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult(result)) sendAction(GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult(result))
@ -1498,7 +1518,7 @@ data class GeneratorState(
* Provides a list of available main types for the generator. * Provides a list of available main types for the generator.
*/ */
val typeOptions: List<MainTypeOption> val typeOptions: List<MainTypeOption>
get() = MainTypeOption.values().toList() get() = MainTypeOption.entries.toList()
/** /**
* Enum representing the main type options for the generator, such as PASSWORD and USERNAME. * Enum representing the main type options for the generator, such as PASSWORD and USERNAME.

View file

@ -2,18 +2,22 @@ package com.x8bit.bitwarden.ui.tools.feature.generator.util
import com.bitwarden.generators.ForwarderServiceType import com.bitwarden.generators.ForwarderServiceType
import com.bitwarden.generators.UsernameGeneratorRequest import com.bitwarden.generators.UsernameGeneratorRequest
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
/** /**
* Converts a [ServiceType] to a [UsernameGeneratorRequest.Forwarded]. * Converts a [ServiceType] to a [UsernameGeneratorRequest.Forwarded].
*/ */
fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded { @Suppress("ReturnCount", "LongMethod")
fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded? {
return when (this) { return when (this) {
is ServiceType.AddyIo -> { is ServiceType.AddyIo -> {
val accessToken = this.apiAccessToken.orNullIfBlank() ?: return null
val domain = this.domainName.orNullIfBlank() ?: return null
UsernameGeneratorRequest.Forwarded( UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.AddyIo( service = ForwarderServiceType.AddyIo(
apiToken = this.apiAccessToken, apiToken = accessToken,
domain = this.domainName, domain = domain,
baseUrl = this.baseUrl, baseUrl = this.baseUrl,
), ),
website = null, website = null,
@ -21,31 +25,51 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
} }
is ServiceType.DuckDuckGo -> { is ServiceType.DuckDuckGo -> {
UsernameGeneratorRequest.Forwarded( this
service = ForwarderServiceType.DuckDuckGo(token = this.apiKey), .apiKey
website = null, .orNullIfBlank()
) ?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.DuckDuckGo(token = it),
website = null,
)
}
} }
is ServiceType.FirefoxRelay -> { is ServiceType.FirefoxRelay -> {
UsernameGeneratorRequest.Forwarded( this
service = ForwarderServiceType.Firefox(apiToken = this.apiAccessToken), .apiAccessToken
website = null, .orNullIfBlank()
) ?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Firefox(apiToken = it),
website = null,
)
}
} }
is ServiceType.FastMail -> { is ServiceType.FastMail -> {
UsernameGeneratorRequest.Forwarded( this
service = ForwarderServiceType.Fastmail(apiToken = this.apiKey), .apiKey
website = null, .orNullIfBlank()
) ?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Fastmail(apiToken = it),
website = null,
)
}
} }
is ServiceType.SimpleLogin -> { is ServiceType.SimpleLogin -> {
UsernameGeneratorRequest.Forwarded( this
service = ForwarderServiceType.SimpleLogin(apiKey = this.apiKey), .apiKey
website = null, .orNullIfBlank()
) ?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.SimpleLogin(apiKey = it),
website = null,
)
}
} }
} }
} }

View file

@ -132,7 +132,10 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(GeneratorAction.SelectClick) viewModel.actionChannel.trySend(GeneratorAction.SelectClick)
assertEquals(GeneratorEvent.NavigateBack, eventTurbine.awaitItem()) assertEquals(GeneratorEvent.NavigateBack, eventTurbine.awaitItem())
assertEquals(GeneratorResult.Username("username"), generatorResultTurbine.awaitItem()) assertEquals(
GeneratorResult.Username(username = "-"),
generatorResultTurbine.awaitItem(),
)
} }
} }
@ -484,6 +487,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val expectedState = val expectedState =
initialPasscodeState.copy( initialPasscodeState.copy(
generatedText = "email+abcd1234@address.com",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail( GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
email = "currentEmail", email = "currentEmail",
@ -558,6 +562,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = initialUsernameState.copy( val expectedState = initialUsernameState.copy(
generatedText = "email+abcd1234@address.com",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState selectedType = GeneratorState
.MainType .MainType
@ -587,6 +592,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = initialUsernameState.copy( val expectedState = initialUsernameState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState.MainType.Username.UsernameType.CatchAllEmail(), selectedType = GeneratorState.MainType.Username.UsernameType.CatchAllEmail(),
), ),
@ -612,6 +618,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = initialUsernameState.copy( val expectedState = initialUsernameState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState selectedType = GeneratorState
.MainType .MainType
@ -639,6 +646,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = initialUsernameState.copy( val expectedState = initialUsernameState.copy(
generatedText = "randomWord",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState.MainType.Username.UsernameType.RandomWord(), selectedType = GeneratorState.MainType.Username.UsernameType.RandomWord(),
), ),
@ -1209,6 +1217,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultForwardedEmailAliasState.copy( val expectedState = defaultForwardedEmailAliasState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState selectedType = GeneratorState
.MainType .MainType
@ -1262,6 +1271,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultAddyIoState.copy( val expectedState = defaultAddyIoState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState selectedServiceType = GeneratorState
@ -1301,6 +1311,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultAddyIoState.copy( val expectedState = defaultAddyIoState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState selectedServiceType = GeneratorState
@ -1351,6 +1362,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultDuckDuckGoState.copy( val expectedState = defaultDuckDuckGoState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState selectedServiceType = GeneratorState
@ -1401,6 +1413,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultFastMailState.copy( val expectedState = defaultFastMailState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState selectedServiceType = GeneratorState
@ -1452,6 +1465,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultFirefoxRelayState.copy( val expectedState = defaultFirefoxRelayState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState selectedServiceType = GeneratorState
@ -1503,6 +1517,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.actionChannel.trySend(action) viewModel.actionChannel.trySend(action)
val expectedState = defaultSimpleLoginState.copy( val expectedState = defaultSimpleLoginState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState selectedServiceType = GeneratorState
@ -1547,6 +1562,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = defaultPlusAddressedEmailState.copy( val expectedState = defaultPlusAddressedEmailState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState selectedType = GeneratorState
.MainType .MainType
@ -1589,6 +1605,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = defaultCatchAllEmailState.copy( val expectedState = defaultCatchAllEmailState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
selectedType = GeneratorState selectedType = GeneratorState
.MainType .MainType
@ -1629,6 +1646,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = defaultRandomWordState.copy( val expectedState = defaultRandomWordState.copy(
generatedText = "randomWord",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord( GeneratorState.MainType.Username.UsernameType.RandomWord(
capitalize = true, capitalize = true,
@ -1654,6 +1672,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
) )
val expectedState = defaultRandomWordState.copy( val expectedState = defaultRandomWordState.copy(
generatedText = "randomWord",
selectedType = GeneratorState.MainType.Username( selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord( GeneratorState.MainType.Username.UsernameType.RandomWord(
includeNumber = true, includeNumber = true,

View file

@ -1,12 +1,38 @@
package com.x8bit.bitwarden.ui.tools.feature.generator.util package com.x8bit.bitwarden.ui.tools.feature.generator.util
import com.bitwarden.generators.ForwarderServiceType import com.bitwarden.generators.ForwarderServiceType
import com.bitwarden.generators.UsernameGeneratorRequest
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class ServiceTypeExtensionsTest { internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for AddyIo returns null when apiAccessToken is blank`() {
val addyIoServiceType = ServiceType.AddyIo(
apiAccessToken = "",
domainName = "test.com",
baseUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest()
assertNull(request)
}
@Test
fun `toUsernameGeneratorRequest for AddyIo returns null when domainName is blank`() {
val addyIoServiceType = ServiceType.AddyIo(
apiAccessToken = "testToken",
domainName = "",
baseUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest()
assertNull(request)
}
@Test @Test
fun `toUsernameGeneratorRequest for AddyIo returns correct request`() { fun `toUsernameGeneratorRequest for AddyIo returns correct request`() {
val addyIoServiceType = ServiceType.AddyIo( val addyIoServiceType = ServiceType.AddyIo(
@ -17,14 +43,24 @@ internal class ServiceTypeExtensionsTest {
val request = addyIoServiceType.toUsernameGeneratorRequest() val request = addyIoServiceType.toUsernameGeneratorRequest()
assertEquals( assertEquals(
ForwarderServiceType.AddyIo( UsernameGeneratorRequest.Forwarded(
apiToken = "testToken", service = ForwarderServiceType.AddyIo(
domain = "test.com", apiToken = "testToken",
baseUrl = "http://test.com", domain = "test.com",
baseUrl = "http://test.com",
),
website = null,
), ),
request.service, request,
) )
assertEquals(null, request.website) }
@Test
fun `toUsernameGeneratorRequest for DuckDuckGo returns null when apiKey is blank`() {
val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "")
val request = duckDuckGoServiceType.toUsernameGeneratorRequest()
assertNull(request)
} }
@Test @Test
@ -32,8 +68,21 @@ internal class ServiceTypeExtensionsTest {
val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "testKey") val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "testKey")
val request = duckDuckGoServiceType.toUsernameGeneratorRequest() val request = duckDuckGoServiceType.toUsernameGeneratorRequest()
assertEquals(ForwarderServiceType.DuckDuckGo("testKey"), request.service) assertEquals(
assertEquals(null, request.website) UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.DuckDuckGo(token = "testKey"),
website = null,
),
request,
)
}
@Test
fun `toUsernameGeneratorRequest for FirefoxRelay returns null when apiAccessToken is blank`() {
val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "")
val request = firefoxRelayServiceType.toUsernameGeneratorRequest()
assertNull(request)
} }
@Test @Test
@ -41,8 +90,21 @@ internal class ServiceTypeExtensionsTest {
val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "testToken") val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "testToken")
val request = firefoxRelayServiceType.toUsernameGeneratorRequest() val request = firefoxRelayServiceType.toUsernameGeneratorRequest()
assertEquals(ForwarderServiceType.Firefox(apiToken = "testToken"), request.service) assertEquals(
assertEquals(null, request.website) UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Firefox(apiToken = "testToken"),
website = null,
),
request,
)
}
@Test
fun `toUsernameGeneratorRequest for FastMail returns null when apiKey is blank`() {
val fastMailServiceType = ServiceType.FastMail(apiKey = "")
val request = fastMailServiceType.toUsernameGeneratorRequest()
assertNull(request)
} }
@Test @Test
@ -50,8 +112,21 @@ internal class ServiceTypeExtensionsTest {
val fastMailServiceType = ServiceType.FastMail(apiKey = "testKey") val fastMailServiceType = ServiceType.FastMail(apiKey = "testKey")
val request = fastMailServiceType.toUsernameGeneratorRequest() val request = fastMailServiceType.toUsernameGeneratorRequest()
assertEquals(ForwarderServiceType.Fastmail(apiToken = "testKey"), request.service) assertEquals(
assertEquals(null, request.website) UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Fastmail(apiToken = "testKey"),
website = null,
),
request,
)
}
@Test
fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "")
val request = simpleLoginServiceType.toUsernameGeneratorRequest()
assertNull(request)
} }
@Test @Test
@ -59,7 +134,12 @@ internal class ServiceTypeExtensionsTest {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "testKey") val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "testKey")
val request = simpleLoginServiceType.toUsernameGeneratorRequest() val request = simpleLoginServiceType.toUsernameGeneratorRequest()
assertEquals(ForwarderServiceType.SimpleLogin(apiKey = "testKey"), request.service) assertEquals(
assertEquals(null, request.website) UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.SimpleLogin(apiKey = "testKey"),
website = null,
),
request,
)
} }
} }