Skip to content

Commit

Permalink
PM-11485: Add routing logic to handle searching during accessibility …
Browse files Browse the repository at this point in the history
…autofill (#3924)
  • Loading branch information
david-livefront authored Sep 17, 2024
1 parent 39df103 commit 765a9eb
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package com.x8bit.bitwarden.ui.platform.feature.search
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.LoginUriView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
Expand Down Expand Up @@ -66,6 +68,7 @@ class SearchViewModel @Inject constructor(
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val policyManager: PolicyManager,
private val accessibilitySelectionManager: AccessibilitySelectionManager,
private val autofillSelectionManager: AutofillSelectionManager,
private val organizationEventManager: OrganizationEventManager,
private val vaultRepo: VaultRepository,
Expand Down Expand Up @@ -160,7 +163,7 @@ class SearchViewModel @Inject constructor(

private fun handleAutofillItemClick(action: SearchAction.AutofillItemClick) {
val cipherView = getCipherViewOrNull(cipherId = action.itemId) ?: return
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView)
useCipherForAutofill(cipherView = cipherView)
}

private fun handleAutofillAndSaveItemClick(action: SearchAction.AutofillAndSaveItemClick) {
Expand Down Expand Up @@ -497,7 +500,7 @@ class SearchViewModel @Inject constructor(
UpdateCipherResult.Success -> {
// Complete the autofill selection flow
val cipherView = getCipherViewOrNull(cipherId = action.cipherId) ?: return
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView)
useCipherForAutofill(cipherView = cipherView)
}
}
}
Expand Down Expand Up @@ -622,6 +625,20 @@ class SearchViewModel @Inject constructor(
}
}

private fun useCipherForAutofill(cipherView: CipherView) {
when (state.autofillSelectionData?.framework) {
AutofillSelectionData.Framework.ACCESSIBILITY -> {
accessibilitySelectionManager.emitAccessibilitySelection(cipherView = cipherView)
}

AutofillSelectionData.Framework.AUTOFILL -> {
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView)
}

null -> Unit
}
}

private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
Expand Down Expand Up @@ -75,6 +77,8 @@ import java.time.ZoneOffset
@Suppress("LargeClass")
class SearchViewModelTest : BaseViewModelTest() {

private val accessibilitySelectionManager: AccessibilitySelectionManager =
AccessibilitySelectionManagerImpl()
private val autofillSelectionManager: AutofillSelectionManager =
AutofillSelectionManagerImpl()

Expand Down Expand Up @@ -206,7 +210,23 @@ class SearchViewModelTest : BaseViewModelTest() {
}

@Test
fun `AutofillItemClick should emit NavigateToViewCipher`() = runTest {
fun `AutofillItemClick should call emitAccessibilitySelection`() = runTest {
val cipherView = setupForAutofill(
autofillSelectionData = AUTOFILL_SELECTION_DATA.copy(
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
),
)
val cipherId = CIPHER_ID
val viewModel = createViewModel()

accessibilitySelectionManager.accessibilitySelectionFlow.test {
viewModel.trySendAction(SearchAction.AutofillItemClick(itemId = cipherId))
assertEquals(cipherView, awaitItem())
}
}

@Test
fun `AutofillItemClick should call emitAutofillSelection`() = runTest {
val cipherView = setupForAutofill()
val cipherId = CIPHER_ID
val viewModel = createViewModel()
Expand Down Expand Up @@ -268,6 +288,64 @@ class SearchViewModelTest : BaseViewModelTest() {
}
}

@Suppress("MaxLineLength")
@Test
fun `AutofillAndSaveItemClick with request success should post to the AccessibilitySelectionManager`() =
runTest {
val autofillSelectionData = AUTOFILL_SELECTION_DATA.copy(
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
)
val cipherView = setupForAutofill(autofillSelectionData = autofillSelectionData)
val cipherId = CIPHER_ID
val updatedCipherView = cipherView.copy(
login = createMockLoginView(number = 1, clock = clock).copy(
uris = listOf(createMockUriView(number = 1)) +
LoginUriView(
uri = AUTOFILL_URI,
match = null,
uriChecksum = null,
),
),
)
val initialState = INITIAL_STATE_FOR_AUTOFILL.copy(
autofillSelectionData = autofillSelectionData,
)
val viewModel = createViewModel()
coEvery {
vaultRepository.updateCipher(
cipherId = cipherId,
cipherView = updatedCipherView,
)
} returns UpdateCipherResult.Success

turbineScope {
val stateTurbine = viewModel
.stateFlow
.testIn(backgroundScope)
val selectionTurbine = accessibilitySelectionManager
.accessibilitySelectionFlow
.testIn(backgroundScope)

assertEquals(initialState, stateTurbine.awaitItem())

viewModel.trySendAction(SearchAction.AutofillAndSaveItemClick(itemId = cipherId))

assertEquals(
initialState.copy(
dialogState = SearchState.DialogState.Loading(
message = R.string.loading.asText(),
),
),
stateTurbine.awaitItem(),
)

assertEquals(initialState, stateTurbine.awaitItem())

// Autofill flow is completed
assertEquals(cipherView, selectionTurbine.awaitItem())
}
}

@Suppress("MaxLineLength")
@Test
fun `AutofillAndSaveItemClick with request success should post to the AutofillSelectionManager`() =
Expand Down Expand Up @@ -397,6 +475,36 @@ class SearchViewModelTest : BaseViewModelTest() {
)
}

@Suppress("MaxLineLength")
@Test
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for autofill should post to the AccessibilitySelectionManager`() =
runTest {
setupMockUri()
val cipherView = setupForAutofill(
autofillSelectionData = AUTOFILL_SELECTION_DATA.copy(
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
),
)
val cipherId = CIPHER_ID
val password = "password"
coEvery {
authRepository.validatePassword(password = password)
} returns ValidatePasswordResult.Success(isValid = true)
val viewModel = createViewModel()

accessibilitySelectionManager.accessibilitySelectionFlow.test {
viewModel.trySendAction(
SearchAction.MasterPasswordRepromptSubmit(
password = password,
masterPasswordRepromptData = MasterPasswordRepromptData.Autofill(
cipherId = cipherId,
),
),
)
assertEquals(cipherView, awaitItem())
}
}

@Suppress("MaxLineLength")
@Test
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for autofill should post to the AutofillSelectionManager`() =
Expand Down Expand Up @@ -1333,6 +1441,7 @@ class SearchViewModelTest : BaseViewModelTest() {
clipboardManager = clipboardManager,
policyManager = policyManager,
specialCircumstanceManager = specialCircumstanceManager,
accessibilitySelectionManager = accessibilitySelectionManager,
autofillSelectionManager = autofillSelectionManager,
organizationEventManager = organizationEventManager,
)
Expand All @@ -1341,9 +1450,11 @@ class SearchViewModelTest : BaseViewModelTest() {
* Generates and returns [CipherView] to be populated for autofill testing and sets up the
* state to return that item.
*/
private fun setupForAutofill(): CipherView {
private fun setupForAutofill(
autofillSelectionData: AutofillSelectionData = AUTOFILL_SELECTION_DATA,
): CipherView {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AutofillSelection(
autofillSelectionData = AUTOFILL_SELECTION_DATA,
autofillSelectionData = autofillSelectionData,
shouldFinishWhenComplete = true,
)
val cipherView = createMockCipherView(
Expand Down

0 comments on commit 765a9eb

Please sign in to comment.