BIT-2183: Pass website with forwarded email username generation (#1249)

This commit is contained in:
Caleb Derosier 2024-04-15 14:31:19 -06:00 committed by Álison Fernandes
parent cec70a9c64
commit 1e980cbfe6
11 changed files with 125 additions and 70 deletions

View file

@ -16,12 +16,13 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
*/
private const val GENERATOR_MODAL_ROUTE_PREFIX: String = "generator_modal"
private const val GENERATOR_MODE_TYPE: String = "generator_mode_type"
private const val GENERATOR_WEBSITE: String = "generator_website"
private const val USERNAME_GENERATOR: String = "username_generator"
private const val PASSWORD_GENERATOR: String = "password_generator"
const val GENERATOR_ROUTE: String = "generator"
private const val GENERATOR_MODAL_ROUTE: String =
"$GENERATOR_MODAL_ROUTE_PREFIX/{$GENERATOR_MODE_TYPE}"
"$GENERATOR_MODAL_ROUTE_PREFIX/{$GENERATOR_MODE_TYPE}?$GENERATOR_WEBSITE={$GENERATOR_WEBSITE}"
/**
* Class to retrieve vault item listing arguments from the [SavedStateHandle].
@ -32,7 +33,10 @@ data class GeneratorArgs(
) {
constructor(savedStateHandle: SavedStateHandle) : this(
type = when (savedStateHandle.get<String>(GENERATOR_MODE_TYPE)) {
USERNAME_GENERATOR -> GeneratorMode.Modal.Username
USERNAME_GENERATOR -> GeneratorMode.Modal.Username(
website = savedStateHandle[GENERATOR_WEBSITE],
)
PASSWORD_GENERATOR -> GeneratorMode.Modal.Password
else -> GeneratorMode.Default
},
@ -63,6 +67,10 @@ fun NavGraphBuilder.generatorModalDestination(
route = GENERATOR_MODAL_ROUTE,
arguments = listOf(
navArgument(GENERATOR_MODE_TYPE) { type = NavType.StringType },
navArgument(GENERATOR_WEBSITE) {
type = NavType.StringType
nullable = true
},
),
) {
GeneratorScreen(
@ -73,7 +81,7 @@ fun NavGraphBuilder.generatorModalDestination(
}
/**
* Navigate to the generator screen in the username generation mode.
* Navigate to the generator screen in the given mode with the corresponding website, if one exists.
*/
fun NavController.navigateToGeneratorModal(
mode: GeneratorMode.Modal,
@ -81,10 +89,11 @@ fun NavController.navigateToGeneratorModal(
) {
val generatorModeType = when (mode) {
GeneratorMode.Modal.Password -> PASSWORD_GENERATOR
GeneratorMode.Modal.Username -> USERNAME_GENERATOR
is GeneratorMode.Modal.Username -> USERNAME_GENERATOR
}
val website = (mode as? GeneratorMode.Modal.Username)?.website
navigate(
route = "$GENERATOR_MODAL_ROUTE_PREFIX/$generatorModeType",
route = "$GENERATOR_MODAL_ROUTE_PREFIX/$generatorModeType?$GENERATOR_WEBSITE=$website",
navOptions = navOptions,
)
}

View file

@ -194,7 +194,9 @@ fun GeneratorScreen(
BitwardenScaffold(
topBar = {
when (state.generatorMode) {
GeneratorMode.Modal.Username, GeneratorMode.Modal.Password ->
is GeneratorMode.Modal.Username,
GeneratorMode.Modal.Password,
-> {
ModalAppBar(
scrollBehavior = scrollBehavior,
onCloseClick = remember(viewModel) {
@ -204,14 +206,16 @@ fun GeneratorScreen(
{ viewModel.trySendAction(GeneratorAction.SelectClick) }
},
)
}
GeneratorMode.Default ->
GeneratorMode.Default -> {
DefaultAppBar(
scrollBehavior = scrollBehavior,
onPasswordHistoryClick = remember(viewModel) {
{ viewModel.trySendAction(GeneratorAction.PasswordHistoryClick) }
},
)
}
}
},
snackbarHost = {

View file

@ -80,20 +80,25 @@ class GeneratorViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val policyManager: PolicyManager,
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
initialState = savedStateHandle[KEY_STATE] ?: GeneratorState(
generatedText = NO_GENERATED_TEXT,
selectedType = when (GeneratorArgs(savedStateHandle).type) {
GeneratorMode.Modal.Username -> Username()
GeneratorMode.Modal.Password -> Passcode()
GeneratorMode.Default -> Passcode(selectedType = Password())
},
generatorMode = GeneratorArgs(savedStateHandle).type,
currentEmailAddress =
requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email),
isUnderPolicy = policyManager
.getActivePolicies<PolicyInformation.PasswordGenerator>()
.any(),
),
initialState = savedStateHandle[KEY_STATE] ?: run {
val generatorMode = GeneratorArgs(savedStateHandle).type
GeneratorState(
generatedText = NO_GENERATED_TEXT,
selectedType = when (generatorMode) {
is GeneratorMode.Modal.Username -> Username()
GeneratorMode.Modal.Password -> Passcode()
GeneratorMode.Default -> Passcode(selectedType = Password())
},
generatorMode = generatorMode,
currentEmailAddress = requireNotNull(
authRepository.userStateFlow.value?.activeAccount?.email,
),
isUnderPolicy = policyManager
.getActivePolicies<PolicyInformation.PasswordGenerator>()
.any(),
website = (generatorMode as? GeneratorMode.Modal.Username)?.website,
)
},
) {
//region Initialization and Overrides
@ -1356,7 +1361,7 @@ class GeneratorViewModel @Inject constructor(
}
private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) {
val request = alias.selectedServiceType?.toUsernameGeneratorRequest() ?: run {
val request = alias.selectedServiceType?.toUsernameGeneratorRequest(state.website) ?: run {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
return
}
@ -1416,6 +1421,7 @@ class GeneratorViewModel @Inject constructor(
)
}
}
private inline fun updatePasswordType(
crossinline block: (Password) -> Password,
) {
@ -1661,6 +1667,7 @@ data class GeneratorState(
val generatorMode: GeneratorMode = GeneratorMode.Default,
val currentEmailAddress: String,
val isUnderPolicy: Boolean = false,
val website: String? = null,
) : Parcelable {
/**

View file

@ -27,8 +27,12 @@ sealed class GeneratorMode : Parcelable {
/**
* Represents the mode for generating usernames.
*
* @property website The website corresponding to this username generation, or empty.
*/
@Parcelize
data object Username : Modal()
data class Username(
val website: String?,
) : Modal()
}
}

View file

@ -9,7 +9,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Us
* Converts a [ServiceType] to a [UsernameGeneratorRequest.Forwarded].
*/
@Suppress("ReturnCount", "LongMethod")
fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded? {
fun ServiceType.toUsernameGeneratorRequest(website: String?): UsernameGeneratorRequest.Forwarded? {
return when (this) {
is ServiceType.AddyIo -> {
val accessToken = this.apiAccessToken.orNullIfBlank() ?: return null
@ -20,7 +20,7 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
domain = domain,
baseUrl = this.baseUrl,
),
website = null,
website = website,
)
}
@ -31,7 +31,7 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.DuckDuckGo(token = it),
website = null,
website = website,
)
}
}
@ -43,7 +43,7 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Firefox(apiToken = it),
website = null,
website = website,
)
}
}
@ -55,7 +55,8 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Fastmail(apiToken = it),
website = null,
// A null `website` value here will cause an error.
website = website.orEmpty(),
)
}
}
@ -65,7 +66,7 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
val domainName = this.domainName.orNullIfBlank() ?: return null
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.ForwardEmail(apiKey, domainName),
website = null,
website = website,
)
}
@ -76,7 +77,7 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
?.let {
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.SimpleLogin(apiKey = it),
website = null,
website = website,
)
}
}

View file

@ -660,7 +660,15 @@ class VaultAddEditViewModel @Inject constructor(
}
private fun handleLoginOpenUsernameGeneratorClick() {
sendEvent(event = VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username))
sendEvent(
event = VaultAddEditEvent.NavigateToGeneratorModal(
generatorMode = GeneratorMode.Modal.Username(
website = (state.viewState as? VaultAddEditState.ViewState.Content)
?.website
.orEmpty(),
),
),
)
}
private fun handleLoginPasswordCheckerClick() {
@ -702,16 +710,6 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleLoginUriSettingsClick() {
viewModelScope.launch {
sendEvent(
event = VaultAddEditEvent.ShowToast(
message = "URI Settings".asText(),
),
)
}
}
private fun handleLoginAddNewUriClick() {
updateLoginContent { loginType ->
loginType.copy(
@ -1663,6 +1661,11 @@ data class VaultAddEditState(
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SECURE_NOTES
}
}
/**
* The first website associated with this item, or null if none exists.
*/
val website: String? get() = (type as? ItemType.Login)?.uriList?.firstOrNull()?.uri
}
}

View file

@ -84,7 +84,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Test
fun `ModalAppBar should be displayed for Username Mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
updateState(
DEFAULT_STATE.copy(
generatorMode = GeneratorMode.Modal.Username(website = null),
),
)
composeTestRule
.onNodeWithContentDescription(label = "Close")
@ -97,7 +101,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Test
fun `on close click should send CloseClick`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
updateState(
DEFAULT_STATE.copy(
generatorMode = GeneratorMode.Modal.Username(website = null),
),
)
composeTestRule
.onNodeWithContentDescription(label = "Close")
@ -110,7 +118,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Test
fun `on select click should send SelectClick`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
updateState(
DEFAULT_STATE.copy(
generatorMode = GeneratorMode.Modal.Username(website = null),
),
)
composeTestRule
.onNodeWithText(text = "Select")
@ -141,7 +153,11 @@ class GeneratorScreenTest : BaseComposeTest() {
@Test
fun `MainTypeOption select control should be hidden for username mode`() {
updateState(DEFAULT_STATE.copy(generatorMode = GeneratorMode.Modal.Username))
updateState(
DEFAULT_STATE.copy(
generatorMode = GeneratorMode.Modal.Username(website = null),
),
)
composeTestRule
.onNodeWithContentDescription(label = "What would you like to generate?, Password")

View file

@ -2137,7 +2137,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
): GeneratorState =
GeneratorState(
generatedText = generatedText,
generatorMode = GeneratorMode.Modal.Username,
generatorMode = GeneratorMode.Modal.Username(website = null),
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
email = email,

View file

@ -16,7 +16,7 @@ internal class ServiceTypeExtensionsTest {
domainName = "test.com",
baseUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest()
val request = addyIoServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -28,7 +28,7 @@ internal class ServiceTypeExtensionsTest {
domainName = "",
baseUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest()
val request = addyIoServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -40,7 +40,8 @@ internal class ServiceTypeExtensionsTest {
domainName = "test.com",
baseUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest()
val website = "bitwarden.com"
val request = addyIoServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@ -49,7 +50,7 @@ internal class ServiceTypeExtensionsTest {
domain = "test.com",
baseUrl = "http://test.com",
),
website = null,
website = website,
),
request,
)
@ -58,7 +59,7 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for DuckDuckGo returns null when apiKey is blank`() {
val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "")
val request = duckDuckGoServiceType.toUsernameGeneratorRequest()
val request = duckDuckGoServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -66,12 +67,13 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for DuckDuckGo returns correct request`() {
val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "testKey")
val request = duckDuckGoServiceType.toUsernameGeneratorRequest()
val website = "bitwarden.com"
val request = duckDuckGoServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.DuckDuckGo(token = "testKey"),
website = null,
website = website,
),
request,
)
@ -80,7 +82,7 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for FirefoxRelay returns null when apiAccessToken is blank`() {
val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "")
val request = firefoxRelayServiceType.toUsernameGeneratorRequest()
val request = firefoxRelayServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -88,12 +90,13 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for FirefoxRelay returns correct request`() {
val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "testToken")
val request = firefoxRelayServiceType.toUsernameGeneratorRequest()
val website = "bitwarden.com"
val request = firefoxRelayServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Firefox(apiToken = "testToken"),
website = null,
website = website,
),
request,
)
@ -102,20 +105,23 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for FastMail returns null when apiKey is blank`() {
val fastMailServiceType = ServiceType.FastMail(apiKey = "")
val request = fastMailServiceType.toUsernameGeneratorRequest()
val request = fastMailServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@Test
fun `toUsernameGeneratorRequest for FastMail returns correct request`() {
val fastMailServiceType = ServiceType.FastMail(apiKey = "testKey")
val request = fastMailServiceType.toUsernameGeneratorRequest()
val fastMailServiceType = ServiceType.FastMail(
apiKey = "testKey",
)
val website = "bitwarden.com"
val request = fastMailServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.Fastmail(apiToken = "testKey"),
website = null,
website = "bitwarden.com",
),
request,
)
@ -127,7 +133,7 @@ internal class ServiceTypeExtensionsTest {
apiKey = "",
domainName = "domainName",
)
val request = forwardMailServiceType.toUsernameGeneratorRequest()
val request = forwardMailServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -138,7 +144,7 @@ internal class ServiceTypeExtensionsTest {
apiKey = "apiKey",
domainName = "",
)
val request = forwardMailServiceType.toUsernameGeneratorRequest()
val request = forwardMailServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -149,7 +155,8 @@ internal class ServiceTypeExtensionsTest {
apiKey = "apiKey",
domainName = "domainName",
)
val request = forwardEmailServiceType.toUsernameGeneratorRequest()
val website = "bitwarden.com"
val request = forwardEmailServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@ -157,7 +164,7 @@ internal class ServiceTypeExtensionsTest {
apiToken = "apiKey",
domain = "domainName",
),
website = null,
website = website,
),
request,
)
@ -166,7 +173,7 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "")
val request = simpleLoginServiceType.toUsernameGeneratorRequest()
val request = simpleLoginServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@ -174,12 +181,13 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for SimpleLogin returns correct request`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "testKey")
val request = simpleLoginServiceType.toUsernameGeneratorRequest()
val website = "bitwarden.com"
val request = simpleLoginServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.SimpleLogin(apiKey = "testKey"),
website = null,
website = website,
),
request,
)

View file

@ -176,12 +176,13 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `on NavigateToGeneratorModal event in username mode should invoke NavigateToGeneratorModal with Username Generator Mode `() {
val website = "bitwarden.com"
mutableEventFlow.tryEmit(
VaultAddEditEvent.NavigateToGeneratorModal(
generatorMode = GeneratorMode.Modal.Username,
generatorMode = GeneratorMode.Modal.Username(website),
),
)
assertEquals(GeneratorMode.Modal.Username, onNavigateToGeneratorModalType)
assertEquals(GeneratorMode.Modal.Username(website), onNavigateToGeneratorModalType)
}
@Test

View file

@ -1160,7 +1160,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick,
)
assertEquals(
VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username),
VaultAddEditEvent.NavigateToGeneratorModal(
GeneratorMode.Modal.Username(website = ""),
),
awaitItem(),
)
}