Ensure more overflow action types get reprompts on Search (#940)

This commit is contained in:
Brian Yencho 2024-02-01 02:04:06 -06:00 committed by Álison Fernandes
parent b800b77194
commit 624e60fd71
4 changed files with 61 additions and 217 deletions

View file

@ -133,28 +133,13 @@ fun SearchContent(
showConfirmationDialog = option
}
is ListingItemOverflowAction.VaultAction.EditClick -> {
if (it.shouldDisplayMasterPasswordReprompt) {
is ListingItemOverflowAction.VaultAction -> {
if (option.requiresPasswordReprompt &&
it.shouldDisplayMasterPasswordReprompt
) {
masterPasswordRepromptData =
MasterPasswordRepromptData(
cipherId = it.id,
type = MasterPasswordRepromptData.Type.Edit,
)
} else {
searchHandlers.onOverflowItemClick(option)
}
}
is ListingItemOverflowAction.VaultAction.CopyPasswordClick -> {
if (it.shouldDisplayMasterPasswordReprompt) {
masterPasswordRepromptData =
MasterPasswordRepromptData(
cipherId = it.id,
type = MasterPasswordRepromptData
.Type
.CopyPassword(
password = option.password,
),
MasterPasswordRepromptData.OverflowItem(
action = option,
)
} else {
searchHandlers.onOverflowItemClick(option)
@ -195,24 +180,19 @@ private fun AutofillSelectionDialog(
onMasterPasswordRepromptRequest: (MasterPasswordRepromptData) -> Unit,
onDismissRequest: () -> Unit,
) {
val selectionCallback: (SearchState.DisplayItem, MasterPasswordRepromptData.Type) -> Unit =
{ item, type ->
val selectionCallback: (SearchState.DisplayItem, MasterPasswordRepromptData) -> Unit =
{ item, data ->
onDismissRequest()
if (item.shouldDisplayMasterPasswordReprompt) {
onMasterPasswordRepromptRequest(
MasterPasswordRepromptData(
cipherId = item.id,
type = type,
),
)
onMasterPasswordRepromptRequest(data)
} else {
when (type) {
MasterPasswordRepromptData.Type.Autofill -> {
onAutofillItemClick(item.id)
when (data) {
is MasterPasswordRepromptData.Autofill -> {
onAutofillItemClick(data.cipherId)
}
MasterPasswordRepromptData.Type.AutofillAndSave -> {
onAutofillAndSaveItemClick(item.id)
is MasterPasswordRepromptData.AutofillAndSave -> {
onAutofillAndSaveItemClick(data.cipherId)
}
else -> Unit
@ -229,7 +209,7 @@ private fun AutofillSelectionDialog(
onClick = {
selectionCallback(
displayItem,
MasterPasswordRepromptData.Type.Autofill,
MasterPasswordRepromptData.Autofill(cipherId = displayItem.id),
)
},
)
@ -240,7 +220,7 @@ private fun AutofillSelectionDialog(
onClick = {
selectionCallback(
displayItem,
MasterPasswordRepromptData.Type.AutofillAndSave,
MasterPasswordRepromptData.AutofillAndSave(cipherId = displayItem.id),
)
},
)

View file

@ -526,42 +526,27 @@ class SearchViewModel @Inject constructor(
data: MasterPasswordRepromptData,
) {
// Complete the deferred actions
val cipherId = data.cipherId
when (val type = data.type) {
MasterPasswordRepromptData.Type.Autofill -> {
when (data) {
is MasterPasswordRepromptData.Autofill -> {
trySendAction(
SearchAction.AutofillItemClick(
itemId = cipherId,
itemId = data.cipherId,
),
)
}
MasterPasswordRepromptData.Type.AutofillAndSave -> {
is MasterPasswordRepromptData.AutofillAndSave -> {
trySendAction(
SearchAction.AutofillAndSaveItemClick(
itemId = cipherId,
itemId = data.cipherId,
),
)
}
MasterPasswordRepromptData.Type.Edit -> {
is MasterPasswordRepromptData.OverflowItem -> {
trySendAction(
SearchAction.OverflowOptionClick(
overflowAction = ListingItemOverflowAction.VaultAction.EditClick(
cipherId = cipherId,
),
),
)
}
is MasterPasswordRepromptData.Type.CopyPassword -> {
trySendAction(
SearchAction.OverflowOptionClick(
overflowAction = ListingItemOverflowAction
.VaultAction
.CopyPasswordClick(
password = type.password,
),
overflowAction = data.action,
),
)
}
@ -1116,44 +1101,31 @@ sealed class SearchEvent {
}
/**
* Data tracking the type of request that triggered a master password reprompt during an autofill
* selection process.
* Data tracking the type of request that triggered a master password reprompt.
*/
@Parcelize
data class MasterPasswordRepromptData(
val cipherId: String,
val type: Type,
) : Parcelable {
sealed class MasterPasswordRepromptData : Parcelable {
/**
* The type of action that requires the prompt.
* Autofill was selected.
*/
sealed class Type : Parcelable {
@Parcelize
data class Autofill(
val cipherId: String,
) : MasterPasswordRepromptData()
/**
* Autofill was selected.
*/
@Parcelize
data object Autofill : Type()
/**
* Autofill-and-save was selected.
*/
@Parcelize
data class AutofillAndSave(
val cipherId: String,
) : MasterPasswordRepromptData()
/**
* Autofill-and-save was selected.
*/
@Parcelize
data object AutofillAndSave : Type()
/**
* Edit was selected.
*/
@Parcelize
data object Edit : Type()
/**
* Copy password was selected.
*/
@Parcelize
data class CopyPassword(
val password: String,
) : Type()
}
/**
* A cipher overflow menu item action was selected.
*/
@Parcelize
data class OverflowItem(
val action: ListingItemOverflowAction.VaultAction,
) : MasterPasswordRepromptData()
}

View file

@ -389,9 +389,8 @@ class SearchScreenTest : BaseComposeTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = "password",
masterPasswordRepromptData = MasterPasswordRepromptData(
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
cipherId = "mockId-1",
type = MasterPasswordRepromptData.Type.Autofill,
),
),
)
@ -425,9 +424,8 @@ class SearchScreenTest : BaseComposeTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = "password",
masterPasswordRepromptData = MasterPasswordRepromptData(
masterPasswordRepromptData = MasterPasswordRepromptData.AutofillAndSave(
cipherId = "mockId-1",
type = MasterPasswordRepromptData.Type.AutofillAndSave,
),
),
)
@ -613,7 +611,7 @@ class SearchScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `on cipher item overflow edit click when reprompt required should show the master password dialog`() {
fun `on cipher item overflow item click when reprompt required should show the master password dialog`() {
mutableStateFlow.update {
it.copy(
viewState = SearchState.ViewState.Content(
@ -642,7 +640,7 @@ class SearchScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `clicking submit on the master password dialog for edit should close the dialog and send MasterPasswordRepromptSubmit`() {
fun `clicking submit on the master password dialog for overflow item should close the dialog and send MasterPasswordRepromptSubmit`() {
mutableStateFlow.update {
it.copy(
viewState = SearchState.ViewState.Content(
@ -676,85 +674,9 @@ class SearchScreenTest : BaseComposeTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = "password",
masterPasswordRepromptData = MasterPasswordRepromptData(
cipherId = "mockId-1",
type = MasterPasswordRepromptData.Type.Edit,
),
),
)
}
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `on cipher item overflow copy password click when reprompt required should show the master password dialog`() {
mutableStateFlow.update {
it.copy(
viewState = SearchState.ViewState.Content(
displayItems = listOf(
createMockDisplayItemForCipher(number = 1)
.copy(shouldDisplayMasterPasswordReprompt = true),
),
),
)
}
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Copy password")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
composeTestRule.assertMasterPasswordDialogDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `clicking submit on the master password dialog for copy password should close the dialog and send MasterPasswordRepromptSubmit`() {
mutableStateFlow.update {
it.copy(
viewState = SearchState.ViewState.Content(
displayItems = listOf(
createMockDisplayItemForCipher(number = 1)
.copy(shouldDisplayMasterPasswordReprompt = true),
),
),
)
}
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Copy password")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
composeTestRule
.onAllNodesWithText(text = "Master password")
.filterToOne(hasAnyAncestor(isDialog()))
.performTextInput("password")
composeTestRule
.onAllNodesWithText(text = "Submit")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = "password",
masterPasswordRepromptData = MasterPasswordRepromptData(
cipherId = "mockId-1",
type = MasterPasswordRepromptData.Type.CopyPassword(
password = "mockPassword-1",
masterPasswordRepromptData = MasterPasswordRepromptData.OverflowItem(
action = ListingItemOverflowAction.VaultAction.EditClick(
cipherId = "mockId-1",
),
),
),

View file

@ -328,9 +328,8 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData(
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
cipherId = cipherId,
type = MasterPasswordRepromptData.Type.Autofill,
),
),
)
@ -366,9 +365,8 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData(
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
cipherId = cipherId,
type = MasterPasswordRepromptData.Type.Autofill,
),
),
)
@ -401,9 +399,8 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData(
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
cipherId = cipherId,
type = MasterPasswordRepromptData.Type.Autofill,
),
),
)
@ -445,9 +442,8 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData(
masterPasswordRepromptData = MasterPasswordRepromptData.AutofillAndSave(
cipherId = cipherId,
type = MasterPasswordRepromptData.Type.AutofillAndSave,
),
),
)
@ -462,7 +458,7 @@ class SearchViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for edit should emit NavigateToEditCipher`() =
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for an overflow action should perform the action`() =
runTest {
val cipherId = "cipherId-1234"
val password = "password"
@ -475,44 +471,18 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData(
cipherId = cipherId,
type = MasterPasswordRepromptData.Type.Edit,
masterPasswordRepromptData = MasterPasswordRepromptData.OverflowItem(
action = ListingItemOverflowAction.VaultAction.EditClick(
cipherId = cipherId,
),
),
),
)
// Edit actions should navigate to the Edit screen
assertEquals(SearchEvent.NavigateToEditCipher(cipherId), awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for copy password should call setText on the ClipboardManager`() =
runTest {
val cipherId = "cipherId-1234"
val password = "password"
val viewModel = createViewModel()
coEvery {
authRepository.validatePassword(password = password)
} returns ValidatePasswordResult.Success(isValid = true)
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData(
cipherId = cipherId,
type = MasterPasswordRepromptData.Type.CopyPassword(
password = password,
),
),
),
)
verify(exactly = 1) {
clipboardManager.setText(password)
}
}
@Test
fun `OverflowOptionClick Send EditClick should emit NavigateToEditSend`() = runTest {
val sendId = "sendId"