diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt index f875ba6c41..f73e18e6b4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt @@ -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 @@ -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, @@ -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) { @@ -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) } } } @@ -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) { updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = false) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index 9ab3637478..09d06af4df 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -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 @@ -75,6 +77,8 @@ import java.time.ZoneOffset @Suppress("LargeClass") class SearchViewModelTest : BaseViewModelTest() { + private val accessibilitySelectionManager: AccessibilitySelectionManager = + AccessibilitySelectionManagerImpl() private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl() @@ -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() @@ -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`() = @@ -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`() = @@ -1333,6 +1441,7 @@ class SearchViewModelTest : BaseViewModelTest() { clipboardManager = clipboardManager, policyManager = policyManager, specialCircumstanceManager = specialCircumstanceManager, + accessibilitySelectionManager = accessibilitySelectionManager, autofillSelectionManager = autofillSelectionManager, organizationEventManager = organizationEventManager, ) @@ -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(