mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-13020: During totp flow master password reprompt should be honored (#4136)
This commit is contained in:
parent
fca00d38f5
commit
c5a266dfc0
13 changed files with 203 additions and 6 deletions
|
@ -116,7 +116,9 @@ fun SearchContent(
|
||||||
supportingLabelTestTag = it.subtitleTestTag,
|
supportingLabelTestTag = it.subtitleTestTag,
|
||||||
optionsTestTag = it.overflowTestTag,
|
optionsTestTag = it.overflowTestTag,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (it.autofillSelectionOptions.isNotEmpty()) {
|
if (it.isTotp && it.shouldDisplayMasterPasswordReprompt) {
|
||||||
|
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(it.id)
|
||||||
|
} else if (it.autofillSelectionOptions.isNotEmpty()) {
|
||||||
autofillSelectionOptionsItem = it
|
autofillSelectionOptionsItem = it
|
||||||
} else {
|
} else {
|
||||||
searchHandlers.onItemClick(it.id)
|
searchHandlers.onItemClick(it.id)
|
||||||
|
|
|
@ -570,6 +570,10 @@ class SearchViewModel @Inject constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is MasterPasswordRepromptData.Totp -> {
|
||||||
|
trySendAction(SearchAction.ItemClick(itemId = data.cipherId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -680,6 +684,7 @@ class SearchViewModel @Inject constructor(
|
||||||
baseIconUrl = state.baseIconUrl,
|
baseIconUrl = state.baseIconUrl,
|
||||||
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||||
isAutofill = state.isAutofill,
|
isAutofill = state.isAutofill,
|
||||||
|
isTotp = state.isTotp,
|
||||||
isPremiumUser = state.isPremium,
|
isPremiumUser = state.isPremium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -826,6 +831,7 @@ data class SearchState(
|
||||||
val overflowOptions: List<ListingItemOverflowAction>,
|
val overflowOptions: List<ListingItemOverflowAction>,
|
||||||
val overflowTestTag: String?,
|
val overflowTestTag: String?,
|
||||||
val autofillSelectionOptions: List<AutofillSelectionOption>,
|
val autofillSelectionOptions: List<AutofillSelectionOption>,
|
||||||
|
val isTotp: Boolean,
|
||||||
val shouldDisplayMasterPasswordReprompt: Boolean,
|
val shouldDisplayMasterPasswordReprompt: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
}
|
}
|
||||||
|
@ -1171,6 +1177,14 @@ sealed class MasterPasswordRepromptData : Parcelable {
|
||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
) : MasterPasswordRepromptData()
|
) : MasterPasswordRepromptData()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autofill was selected.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Totp(
|
||||||
|
val cipherId: String,
|
||||||
|
) : MasterPasswordRepromptData()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cipher overflow menu item action was selected.
|
* A cipher overflow menu item action was selected.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -148,6 +148,7 @@ fun List<CipherView>.toViewState(
|
||||||
hasMasterPassword: Boolean,
|
hasMasterPassword: Boolean,
|
||||||
isIconLoadingDisabled: Boolean,
|
isIconLoadingDisabled: Boolean,
|
||||||
isAutofill: Boolean,
|
isAutofill: Boolean,
|
||||||
|
isTotp: Boolean,
|
||||||
isPremiumUser: Boolean,
|
isPremiumUser: Boolean,
|
||||||
): SearchState.ViewState =
|
): SearchState.ViewState =
|
||||||
when {
|
when {
|
||||||
|
@ -159,6 +160,7 @@ fun List<CipherView>.toViewState(
|
||||||
hasMasterPassword = hasMasterPassword,
|
hasMasterPassword = hasMasterPassword,
|
||||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||||
isAutofill = isAutofill,
|
isAutofill = isAutofill,
|
||||||
|
isTotp = isTotp,
|
||||||
isPremiumUser = isPremiumUser,
|
isPremiumUser = isPremiumUser,
|
||||||
)
|
)
|
||||||
.sortAlphabetically(),
|
.sortAlphabetically(),
|
||||||
|
@ -172,11 +174,13 @@ fun List<CipherView>.toViewState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
private fun List<CipherView>.toDisplayItemList(
|
private fun List<CipherView>.toDisplayItemList(
|
||||||
baseIconUrl: String,
|
baseIconUrl: String,
|
||||||
hasMasterPassword: Boolean,
|
hasMasterPassword: Boolean,
|
||||||
isIconLoadingDisabled: Boolean,
|
isIconLoadingDisabled: Boolean,
|
||||||
isAutofill: Boolean,
|
isAutofill: Boolean,
|
||||||
|
isTotp: Boolean,
|
||||||
isPremiumUser: Boolean,
|
isPremiumUser: Boolean,
|
||||||
): List<SearchState.DisplayItem> =
|
): List<SearchState.DisplayItem> =
|
||||||
this.map {
|
this.map {
|
||||||
|
@ -185,15 +189,18 @@ private fun List<CipherView>.toDisplayItemList(
|
||||||
hasMasterPassword = hasMasterPassword,
|
hasMasterPassword = hasMasterPassword,
|
||||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||||
isAutofill = isAutofill,
|
isAutofill = isAutofill,
|
||||||
|
isTotp = isTotp,
|
||||||
isPremiumUser = isPremiumUser,
|
isPremiumUser = isPremiumUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
private fun CipherView.toDisplayItem(
|
private fun CipherView.toDisplayItem(
|
||||||
baseIconUrl: String,
|
baseIconUrl: String,
|
||||||
hasMasterPassword: Boolean,
|
hasMasterPassword: Boolean,
|
||||||
isIconLoadingDisabled: Boolean,
|
isIconLoadingDisabled: Boolean,
|
||||||
isAutofill: Boolean,
|
isAutofill: Boolean,
|
||||||
|
isTotp: Boolean,
|
||||||
isPremiumUser: Boolean,
|
isPremiumUser: Boolean,
|
||||||
): SearchState.DisplayItem =
|
): SearchState.DisplayItem =
|
||||||
SearchState.DisplayItem(
|
SearchState.DisplayItem(
|
||||||
|
@ -221,6 +228,7 @@ private fun CipherView.toDisplayItem(
|
||||||
.filter {
|
.filter {
|
||||||
this.login != null || (it != AutofillSelectionOption.AUTOFILL_AND_SAVE)
|
this.login != null || (it != AutofillSelectionOption.AUTOFILL_AND_SAVE)
|
||||||
},
|
},
|
||||||
|
isTotp = isTotp,
|
||||||
shouldDisplayMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
shouldDisplayMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -360,6 +368,7 @@ private fun SendView.toDisplayItem(
|
||||||
overflowTestTag = "SendOptionsButton",
|
overflowTestTag = "SendOptionsButton",
|
||||||
totpCode = null,
|
totpCode = null,
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
|
isTotp = false,
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -215,9 +215,12 @@ fun VaultItemListingContent(
|
||||||
supportingLabelTestTag = it.subtitleTestTag,
|
supportingLabelTestTag = it.subtitleTestTag,
|
||||||
optionsTestTag = it.optionsTestTag,
|
optionsTestTag = it.optionsTestTag,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (it.isAutofill && it.shouldShowMasterPasswordReprompt) {
|
if (it.isTotp && it.shouldShowMasterPasswordReprompt) {
|
||||||
masterPasswordRepromptData =
|
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(
|
||||||
MasterPasswordRepromptData.Autofill(
|
cipherId = it.id,
|
||||||
|
)
|
||||||
|
} else if (it.isAutofill && it.shouldShowMasterPasswordReprompt) {
|
||||||
|
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
|
||||||
cipherId = it.id,
|
cipherId = it.id,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1153,6 +1153,10 @@ class VaultItemListingViewModel @Inject constructor(
|
||||||
VaultItemListingsAction.OverflowOptionClick(data.action),
|
VaultItemListingsAction.OverflowOptionClick(data.action),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is MasterPasswordRepromptData.Totp -> {
|
||||||
|
sendEvent(VaultItemListingEvent.NavigateToEditCipher(data.cipherId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1972,6 +1976,7 @@ data class VaultItemListingState(
|
||||||
val optionsTestTag: String,
|
val optionsTestTag: String,
|
||||||
val isAutofill: Boolean,
|
val isAutofill: Boolean,
|
||||||
val isFido2Creation: Boolean,
|
val isFido2Creation: Boolean,
|
||||||
|
val isTotp: Boolean,
|
||||||
val shouldShowMasterPasswordReprompt: Boolean,
|
val shouldShowMasterPasswordReprompt: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2551,6 +2556,14 @@ sealed class MasterPasswordRepromptData : Parcelable {
|
||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
) : MasterPasswordRepromptData()
|
) : MasterPasswordRepromptData()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Totp was selected.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Totp(
|
||||||
|
val cipherId: String,
|
||||||
|
) : MasterPasswordRepromptData()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cipher overflow menu item action was selected.
|
* A cipher overflow menu item action was selected.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -137,6 +137,7 @@ fun VaultData.toViewState(
|
||||||
isFido2Creation = fido2CreationData != null,
|
isFido2Creation = fido2CreationData != null,
|
||||||
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
|
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
|
||||||
isPremiumUser = isPremiumUser,
|
isPremiumUser = isPremiumUser,
|
||||||
|
isTotp = totpData != null,
|
||||||
),
|
),
|
||||||
displayFolderList = folderList.map { folderView ->
|
displayFolderList = folderList.map { folderView ->
|
||||||
VaultItemListingState.FolderDisplayItem(
|
VaultItemListingState.FolderDisplayItem(
|
||||||
|
@ -282,6 +283,7 @@ private fun List<CipherView>.toDisplayItemList(
|
||||||
isFido2Creation: Boolean,
|
isFido2Creation: Boolean,
|
||||||
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
|
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
|
||||||
isPremiumUser: Boolean,
|
isPremiumUser: Boolean,
|
||||||
|
isTotp: Boolean,
|
||||||
): List<VaultItemListingState.DisplayItem> =
|
): List<VaultItemListingState.DisplayItem> =
|
||||||
this.map {
|
this.map {
|
||||||
it.toDisplayItem(
|
it.toDisplayItem(
|
||||||
|
@ -295,6 +297,7 @@ private fun List<CipherView>.toDisplayItemList(
|
||||||
fido2CredentialAutofillView.cipherId == it.id
|
fido2CredentialAutofillView.cipherId == it.id
|
||||||
},
|
},
|
||||||
isPremiumUser = isPremiumUser,
|
isPremiumUser = isPremiumUser,
|
||||||
|
isTotp = isTotp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,6 +321,7 @@ private fun CipherView.toDisplayItem(
|
||||||
isFido2Creation: Boolean,
|
isFido2Creation: Boolean,
|
||||||
fido2CredentialAutofillView: Fido2CredentialAutofillView?,
|
fido2CredentialAutofillView: Fido2CredentialAutofillView?,
|
||||||
isPremiumUser: Boolean,
|
isPremiumUser: Boolean,
|
||||||
|
isTotp: Boolean,
|
||||||
): VaultItemListingState.DisplayItem =
|
): VaultItemListingState.DisplayItem =
|
||||||
VaultItemListingState.DisplayItem(
|
VaultItemListingState.DisplayItem(
|
||||||
id = id.orEmpty(),
|
id = id.orEmpty(),
|
||||||
|
@ -345,6 +349,7 @@ private fun CipherView.toDisplayItem(
|
||||||
optionsTestTag = "CipherOptionsButton",
|
optionsTestTag = "CipherOptionsButton",
|
||||||
isAutofill = isAutofill,
|
isAutofill = isAutofill,
|
||||||
isFido2Creation = isFido2Creation,
|
isFido2Creation = isFido2Creation,
|
||||||
|
isTotp = isTotp,
|
||||||
shouldShowMasterPasswordReprompt = (reprompt == CipherRepromptType.PASSWORD) &&
|
shouldShowMasterPasswordReprompt = (reprompt == CipherRepromptType.PASSWORD) &&
|
||||||
hasMasterPassword,
|
hasMasterPassword,
|
||||||
)
|
)
|
||||||
|
@ -416,6 +421,7 @@ private fun SendView.toDisplayItem(
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@get:DrawableRes
|
@get:DrawableRes
|
||||||
|
|
|
@ -345,6 +345,26 @@ class SearchScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.assertNoDialogExists()
|
composeTestRule.assertNoDialogExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on totp when reprompt is required should show master password dialog`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||||
|
viewState = SearchState.ViewState.Content(
|
||||||
|
displayItems = listOf(
|
||||||
|
createMockDisplayItemForCipher(number = 1, isTotp = true).copy(
|
||||||
|
shouldDisplayMasterPasswordReprompt = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
totpData = mockk(),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "mockName-1")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule.assertMasterPasswordDialogDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking cancel on the master password dialog should close the dialog`() {
|
fun `clicking cancel on the master password dialog should close the dialog`() {
|
||||||
mutableStateFlow.value = createStateForAutofill(isRepromptRequired = true)
|
mutableStateFlow.value = createStateForAutofill(isRepromptRequired = true)
|
||||||
|
|
|
@ -592,6 +592,32 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for totp should emit NavigateToEditCipher`() =
|
||||||
|
runTest {
|
||||||
|
setupMockUri()
|
||||||
|
val cipherId = CIPHER_ID
|
||||||
|
val password = "password"
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePassword(password = password)
|
||||||
|
} returns ValidatePasswordResult.Success(isValid = true)
|
||||||
|
val viewModel = createViewModel(initialState = DEFAULT_STATE.copy(totpData = mockk()))
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
SearchAction.MasterPasswordRepromptSubmit(
|
||||||
|
password = password,
|
||||||
|
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(
|
||||||
|
cipherId = cipherId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(SearchEvent.NavigateToEditCipher(cipherId), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for an overflow action should perform the action`() =
|
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for an overflow action should perform the action`() =
|
||||||
|
@ -990,6 +1016,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
} returns expectedViewState
|
} returns expectedViewState
|
||||||
val dataState = DataState.Loaded(
|
val dataState = DataState.Loaded(
|
||||||
|
@ -1092,6 +1119,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
} returns expectedViewState
|
} returns expectedViewState
|
||||||
mutableVaultDataStateFlow.tryEmit(
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
@ -1204,6 +1232,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
} returns expectedViewState
|
} returns expectedViewState
|
||||||
val dataState = DataState.Error(
|
val dataState = DataState.Error(
|
||||||
|
@ -1319,6 +1348,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
} returns expectedViewState
|
} returns expectedViewState
|
||||||
val dataState = DataState.NoNetwork(
|
val dataState = DataState.NoNetwork(
|
||||||
|
@ -1494,6 +1524,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
isAutofill = true,
|
isAutofill = true,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
} returns expectedViewState
|
} returns expectedViewState
|
||||||
val dataState = DataState.Loaded(
|
val dataState = DataState.Loaded(
|
||||||
|
|
|
@ -299,6 +299,7 @@ class SearchTypeDataExtensionsTest {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(SearchState.ViewState.Empty(message = null), result)
|
assertEquals(SearchState.ViewState.Empty(message = null), result)
|
||||||
|
@ -324,6 +325,7 @@ class SearchTypeDataExtensionsTest {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -364,6 +366,7 @@ class SearchTypeDataExtensionsTest {
|
||||||
isAutofill = true,
|
isAutofill = true,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -414,6 +417,7 @@ class SearchTypeDataExtensionsTest {
|
||||||
isAutofill = false,
|
isAutofill = false,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
|
isTotp = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflo
|
||||||
fun createMockDisplayItemForCipher(
|
fun createMockDisplayItemForCipher(
|
||||||
number: Int,
|
number: Int,
|
||||||
cipherType: CipherType = CipherType.LOGIN,
|
cipherType: CipherType = CipherType.LOGIN,
|
||||||
|
isTotp: Boolean = false,
|
||||||
): SearchState.DisplayItem =
|
): SearchState.DisplayItem =
|
||||||
when (cipherType) {
|
when (cipherType) {
|
||||||
CipherType.LOGIN -> {
|
CipherType.LOGIN -> {
|
||||||
|
@ -65,6 +66,7 @@ fun createMockDisplayItemForCipher(
|
||||||
totpCode = "mockTotp-$number",
|
totpCode = "mockTotp-$number",
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = isTotp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +104,7 @@ fun createMockDisplayItemForCipher(
|
||||||
totpCode = null,
|
totpCode = null,
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +148,7 @@ fun createMockDisplayItemForCipher(
|
||||||
totpCode = null,
|
totpCode = null,
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +183,7 @@ fun createMockDisplayItemForCipher(
|
||||||
totpCode = null,
|
totpCode = null,
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,6 +232,7 @@ fun createMockDisplayItemForSend(
|
||||||
totpCode = null,
|
totpCode = null,
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,6 +271,7 @@ fun createMockDisplayItemForSend(
|
||||||
totpCode = null,
|
totpCode = null,
|
||||||
autofillSelectionOptions = emptyList(),
|
autofillSelectionOptions = emptyList(),
|
||||||
shouldDisplayMasterPasswordReprompt = false,
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1052,6 +1052,58 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `clicking on a display item when master password reprompt is required for totp flow should show the master password dialog`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = VaultItemListingState.ViewState.Content(
|
||||||
|
displayCollectionList = emptyList(),
|
||||||
|
displayItemList = listOf(
|
||||||
|
createDisplayItem(number = 1).copy(
|
||||||
|
isTotp = true,
|
||||||
|
shouldShowMasterPasswordReprompt = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
displayFolderList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "mockTitle-1")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Master password confirmation")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(
|
||||||
|
text = "This action is protected, to continue please re-enter your master " +
|
||||||
|
"password to verify your identity.",
|
||||||
|
)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Master password")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Cancel")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Submit")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
viewModel.trySendAction(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking cancel on the master password dialog should close the dialog`() {
|
fun `clicking cancel on the master password dialog should close the dialog`() {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
|
@ -2146,6 +2198,7 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = null,
|
iconTestTag = null,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createCipherDisplayItem(number: Int): VaultItemListingState.DisplayItem =
|
private fun createCipherDisplayItem(number: Int): VaultItemListingState.DisplayItem =
|
||||||
|
@ -2170,4 +2223,5 @@ private fun createCipherDisplayItem(number: Int): VaultItemListingState.DisplayI
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = null,
|
iconTestTag = null,
|
||||||
|
isTotp = true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -917,6 +917,31 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `MasterPasswordRepromptSubmit with a valid password for totp flow should emit NavigateToEditCipher`() =
|
||||||
|
runTest {
|
||||||
|
val cipherId = "cipherId-1234"
|
||||||
|
val password = "password"
|
||||||
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePassword(password = password)
|
||||||
|
} returns ValidatePasswordResult.Success(isValid = true)
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemListingsAction.MasterPasswordRepromptSubmit(
|
||||||
|
password = password,
|
||||||
|
masterPasswordRepromptData = MasterPasswordRepromptData.Totp(
|
||||||
|
cipherId = cipherId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// An Edit action navigates to the Edit screen
|
||||||
|
assertEquals(VaultItemListingEvent.NavigateToEditCipher(cipherId), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `AddVaultItemClick for vault item should emit NavigateToAddVaultItem`() = runTest {
|
fun `AddVaultItemClick for vault item should emit NavigateToAddVaultItem`() = runTest {
|
||||||
val viewModel = createVaultItemListingViewModel()
|
val viewModel = createVaultItemListingViewModel()
|
||||||
|
@ -1467,6 +1492,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
createMockDisplayItemForCipher(
|
createMockDisplayItemForCipher(
|
||||||
number = 1,
|
number = 1,
|
||||||
secondSubtitleTestTag = "PasskeySite",
|
secondSubtitleTestTag = "PasskeySite",
|
||||||
|
isTotp = true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
displayFolderList = emptyList(),
|
displayFolderList = emptyList(),
|
||||||
|
|
|
@ -12,12 +12,14 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflo
|
||||||
/**
|
/**
|
||||||
* Create a mock [VaultItemListingState.DisplayItem] with a given [number].
|
* Create a mock [VaultItemListingState.DisplayItem] with a given [number].
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
fun createMockDisplayItemForCipher(
|
fun createMockDisplayItemForCipher(
|
||||||
number: Int,
|
number: Int,
|
||||||
cipherType: CipherType = CipherType.LOGIN,
|
cipherType: CipherType = CipherType.LOGIN,
|
||||||
subtitle: String? = "mockUsername-$number",
|
subtitle: String? = "mockUsername-$number",
|
||||||
secondSubtitleTestTag: String? = null,
|
secondSubtitleTestTag: String? = null,
|
||||||
requiresPasswordReprompt: Boolean = true,
|
requiresPasswordReprompt: Boolean = true,
|
||||||
|
isTotp: Boolean = false,
|
||||||
): VaultItemListingState.DisplayItem =
|
): VaultItemListingState.DisplayItem =
|
||||||
when (cipherType) {
|
when (cipherType) {
|
||||||
CipherType.LOGIN -> {
|
CipherType.LOGIN -> {
|
||||||
|
@ -71,6 +73,7 @@ fun createMockDisplayItemForCipher(
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = "LoginCipherIcon",
|
iconTestTag = "LoginCipherIcon",
|
||||||
|
isTotp = isTotp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +114,7 @@ fun createMockDisplayItemForCipher(
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = "SecureNoteCipherIcon",
|
iconTestTag = "SecureNoteCipherIcon",
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,6 +161,7 @@ fun createMockDisplayItemForCipher(
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = "CardCipherIcon",
|
iconTestTag = "CardCipherIcon",
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +199,7 @@ fun createMockDisplayItemForCipher(
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = "IdentityCipherIcon",
|
iconTestTag = "IdentityCipherIcon",
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +251,7 @@ fun createMockDisplayItemForSend(
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = null,
|
iconTestTag = null,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,6 +293,7 @@ fun createMockDisplayItemForSend(
|
||||||
isFido2Creation = false,
|
isFido2Creation = false,
|
||||||
shouldShowMasterPasswordReprompt = false,
|
shouldShowMasterPasswordReprompt = false,
|
||||||
iconTestTag = null,
|
iconTestTag = null,
|
||||||
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue