diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/Fido2ViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/Fido2ViewModel.kt new file mode 100644 index 000000000..b24941d66 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/fido2/Fido2ViewModel.kt @@ -0,0 +1,315 @@ +package com.x8bit.bitwarden.ui.autofill.fido2 + +import androidx.lifecycle.viewModelScope +import com.bitwarden.vault.CipherView +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest +import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager +import com.x8bit.bitwarden.data.platform.manager.util.toFido2GetCredentialsRequestOrNull +import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.repository.model.VaultData +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Models logic for [Fido2Screen]. + */ +@Suppress("LargeClass") +@HiltViewModel +class Fido2ViewModel @Inject constructor( + private val authRepository: AuthRepository, + vaultRepository: VaultRepository, + private val fido2CredentialManager: Fido2CredentialManager, + private val specialCircumstanceManager: SpecialCircumstanceManager, +) : BaseViewModel( + initialState = run { + val specialCircumstance = specialCircumstanceManager.specialCircumstance + val fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull() + val requestUserId = fido2GetCredentialsRequest?.userId + Fido2State( + requestUserId = requireNotNull(requestUserId), + fido2GetCredentialsRequest = fido2GetCredentialsRequest, + dialog = null, + ) + }, +) { + + init { + vaultRepository.vaultDataStateFlow + .map { Fido2Action.Internal.VaultDataStateChangeReceive(it) } + .onEach(::trySendAction) + .launchIn(viewModelScope) + + authRepository.userStateFlow + .map { Fido2Action.Internal.UserStateChangeReceive(it) } + .onEach(::trySendAction) + .launchIn(viewModelScope) + } + + override fun handleAction(action: Fido2Action) { + when (action) { + is Fido2Action.DismissErrorDialogClick -> { + clearDialogState() + sendEvent( + Fido2Event.CompleteFido2GetCredentialsRequest( + result = action.result, + ), + ) + } + + is Fido2Action.Internal -> { + handleInternalAction(action) + } + } + } + + private fun handleInternalAction(action: Fido2Action.Internal) { + when (action) { + is Fido2Action.Internal.VaultDataStateChangeReceive -> { + handleVaultDataStateChangeReceive(action.vaultData) + } + + is Fido2Action.Internal.UserStateChangeReceive -> { + handleUserStateChangeReceive(action) + } + } + } + + private fun handleUserStateChangeReceive(action: Fido2Action.Internal.UserStateChangeReceive) { + val activeUserId = action.userState?.activeUserId ?: return + val requestUserId = state.requestUserId + if (requestUserId != activeUserId) { + authRepository.switchAccount(requestUserId) + } + } + + private fun handleVaultDataStateChangeReceive(vaultDataState: DataState) { + when (vaultDataState) { + is DataState.Error -> mutableStateFlow.update { + it.copy( + dialog = Fido2State.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.generic_error_message.asText(), + ), + ) + } + + is DataState.Loaded -> handleVaultDataLoaded() + DataState.Loading -> handleVaultDataLoading() + is DataState.NoNetwork -> handleNoNetwork() + is DataState.Pending -> clearDialogState() + } + } + + private fun handleVaultDataLoaded() { + clearDialogState() + if (authRepository.activeUserId != state.requestUserId) { + // Ignore vault data when we are waiting for the account to switch + return + } + + viewModelScope.launch { + state + .fido2GetCredentialsRequest + ?.let { getCredentialsRequest -> + getFido2CredentialAutofillViewsForSelection(getCredentialsRequest) + } + } + } + + private suspend fun getFido2CredentialAutofillViewsForSelection( + fido2GetCredentialsRequest: Fido2GetCredentialsRequest, + ) { + val getCredentialsResult = fido2CredentialManager + .getFido2CredentialsForRelyingParty(fido2GetCredentialsRequest) + + when (getCredentialsResult) { + is Fido2GetCredentialsResult.Error -> { + showFido2ErrorDialog( + title = R.string.an_error_has_occurred.asText(), + message = R.string.passkey_operation_failed_because_passkey_does_not_exist + .asText(), + ) + } + + Fido2GetCredentialsResult.Cancelled, + is Fido2GetCredentialsResult.Success, + -> { + sendEvent( + Fido2Event.CompleteFido2GetCredentialsRequest( + result = getCredentialsResult, + ), + ) + } + } + + sendEvent( + Fido2Event.CompleteFido2GetCredentialsRequest( + result = getCredentialsResult, + ), + ) + } + + private fun handleVaultDataLoading() { + mutableStateFlow.update { it.copy(dialog = Fido2State.DialogState.Loading) } + } + + private fun handleNoNetwork() { + mutableStateFlow.update { + it.copy( + dialog = Fido2State.DialogState.Error( + R.string.internet_connection_required_title.asText(), + R.string.internet_connection_required_message.asText(), + ), + ) + } + } + + private fun showFido2ErrorDialog(title: Text, message: Text) { + mutableStateFlow.update { + it.copy( + dialog = Fido2State.DialogState.Error(title, message), + ) + } + } + + private fun clearDialogState() { + mutableStateFlow.update { it.copy(dialog = null) } + } +} + +/** + * Represents the UI state for [Fido2Screen]. + * + * @property requestUserId User ID contained within the FIDO 2 request. + * @property fido2GetCredentialsRequest Data required to discover FIDO 2 credential. + */ +data class Fido2State( + val requestUserId: String, + val fido2GetCredentialsRequest: Fido2GetCredentialsRequest?, + val dialog: DialogState?, +) { + + /** + * Represents the dialog UI state for [Fido2Screen]. + */ + sealed class DialogState { + /** + * Displays a loading dialog. + */ + data object Loading : DialogState() + + /** + * Displays a generic error dialog with a [title] and [message]. + */ + data class Error(val title: Text, val message: Text) : DialogState() + + /** + * Displays a PIN entry dialog to verify the user. + */ + data class Fido2PinPrompt(val selectedCipherId: String) : DialogState() + + /** + * Displays a master password entry dialog to verify the user. + */ + data class Fido2MasterPasswordPrompt(val selectedCipherId: String) : DialogState() + + /** + * Displays a PIN creation dialog for user verification. + */ + data class Fido2PinSetUpPrompt(val selectedCipherId: String) : DialogState() + + /** + * Displays a master password validation error dialog. + */ + data class Fido2MasterPasswordError( + val title: Text?, + val message: Text, + val selectedCipherId: String, + ) : DialogState() + + /** + * Displays a PIN set up error dialog. + */ + data class Fido2PinSetUpError( + val title: Text?, + val message: Text, + val selectedCipherId: String, + ) : DialogState() + + /** + * Displays a PIN validation error dialog. + */ + data class Fido2PinError( + val title: Text?, + val message: Text, + val selectedCipherId: String, + ) : DialogState() + } +} + +/** + * Models events for [Fido2Screen]. + */ +sealed class Fido2Event { + + /** + * Completes FIDO 2 credential discovery with the given [result]. + */ + data class CompleteFido2GetCredentialsRequest( + val result: Fido2GetCredentialsResult, + ) : BackgroundEvent, Fido2Event() + + /** + * Performs device based user verification. + */ + data class Fido2UserVerification( + val required: Boolean, + val selectedCipher: CipherView, + ) : BackgroundEvent, Fido2Event() +} + +/** + * Models actions for [Fido2Screen]. + */ +sealed class Fido2Action { + + /** + * Indicates the user dismissed the error dialog. + */ + data class DismissErrorDialogClick(val result: Fido2GetCredentialsResult) : Fido2Action() + + /** + * Models actions [Fido2ViewModel] may itself send. + */ + sealed class Internal : Fido2Action() { + + /** + * Indicates the [userState] has changed. + */ + data class UserStateChangeReceive( + val userState: UserState?, + ) : Internal() + + /** + * Indicates the [vaultData] has changed. + */ + data class VaultDataStateChangeReceive( + val vaultData: DataState, + ) : Internal() + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index efa2b0301..6e6c33565 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -5,7 +5,6 @@ import androidx.annotation.DrawableRes import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.fido.Fido2CredentialAutofillView -import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -170,7 +169,6 @@ class VaultItemListingViewModel @Inject constructor( it .filterForAutofillIfNecessary() .filterForFido2CreationIfNecessary() - .filterForFidoGetCredentialsIfNecessary() .filterForTotpIfNecessary(), ) } @@ -518,6 +516,7 @@ class VaultItemListingViewModel @Inject constructor( VaultItemListingEvent.NavigateToAddVaultItem( vaultItemCipherType = itemListingType.toVaultItemCipherType(), selectedFolderId = itemListingType.folderId, + selectedCollectionId = null, ) } @@ -688,16 +687,6 @@ class VaultItemListingViewModel @Inject constructor( } } - private fun authenticateFido2Credential(request: Fido2CredentialAssertionRequest) { - viewModelScope.launch { - sendAction( - VaultItemListingsAction.Internal.Fido2AssertionResultReceive( - result = fido2CredentialManager.authenticateFido2Credential(request), - ), - ) - } - } - private fun handleMasterPasswordRepromptSubmit( action: VaultItemListingsAction.MasterPasswordRepromptSubmit, ) { @@ -940,14 +929,6 @@ class VaultItemListingViewModel @Inject constructor( is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> { handleFido2RegisterCredentialResultReceive(action) } - - is VaultItemListingsAction.Internal.Fido2AssertionDataReceive -> { - handleFido2AssertionDataReceive(action) - } - - is VaultItemListingsAction.Internal.Fido2AssertionResultReceive -> { - handleFido2AssertionResultReceive(action) - } } } @@ -1212,10 +1193,6 @@ class VaultItemListingViewModel @Inject constructor( cipherView = cipherView, ) } - ?: specialCircumstanceManager - .specialCircumstance - ?.toFido2AssertionRequestOrNull() - ?.let { authenticateFido2Credential(request = it) } ?: showFido2ErrorDialog() } //endregion VaultItemListing Handlers @@ -1238,31 +1215,7 @@ class VaultItemListingViewModel @Inject constructor( private fun vaultLoadedReceive(vaultData: DataState.Loaded) { updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true) - state - // TODO: Move FIDO 2 get credentials to vault unlock screen - .fido2GetCredentialsRequest - ?.let { fido2GetCredentialsRequest -> - viewModelScope.launch { - sendEvent( - VaultItemListingEvent.CompleteFido2GetCredentialsRequest( - fido2CredentialManager.getFido2CredentialsForRelyingParty( - fido2GetCredentialsRequest = fido2GetCredentialsRequest, - ), - ), - ) - } - } - ?: state - // TODO: Move FIDO 2 authentication to vault unlock screen - .fido2CredentialAssertionRequest - ?.let { fido2AssertionRequest -> - trySendAction( - VaultItemListingsAction.Internal.Fido2AssertionDataReceive( - fido2AssertionRequest, - ), - ) - } - ?: mutableStateFlow.update { it.copy(isRefreshing = false) } + mutableStateFlow.update { it.copy(isRefreshing = false) } } private fun vaultLoadingReceive() { @@ -1324,43 +1277,6 @@ class VaultItemListingViewModel @Inject constructor( sendEvent(VaultItemListingEvent.CompleteFido2Registration(action.result)) } - private fun handleFido2AssertionDataReceive( - action: VaultItemListingsAction.Internal.Fido2AssertionDataReceive, - ) { - mutableStateFlow.update { - it.copy( - dialogState = VaultItemListingState.DialogState.Loading( - message = R.string.loading.asText(), - ), - ) - } - val request = action.data - val ciphers = vaultRepository - .ciphersStateFlow - .value - .data - .orEmpty() - .filter { it.isActiveWithFido2Credentials } - if (request.cipherId.isNullOrEmpty()) { - showFido2ErrorDialog() - } else { - val selectedCipher = ciphers - .find { it.id == request.cipherId } - ?: run { - showFido2ErrorDialog() - return - } - - if (state.hasMasterPassword && - selectedCipher.reprompt == CipherRepromptType.PASSWORD - ) { - repromptMasterPasswordForFido2Assertion(request.cipherId) - } else { - verifyUserAndAuthenticateCredential(request, selectedCipher) - } - } - } - private fun repromptMasterPasswordForFido2Assertion(cipherId: String) { mutableStateFlow.update { it.copy( @@ -1371,43 +1287,6 @@ class VaultItemListingViewModel @Inject constructor( } } - private fun verifyUserAndAuthenticateCredential( - request: Fido2CredentialAssertionRequest, - selectedCipher: CipherView, - ) { - if (fido2CredentialManager.isUserVerified) { - authenticateFido2Credential(request) - return - } - - val userVerificationRequirement: UserVerificationRequirement = fido2CredentialManager - .getUserVerificationRequirementForAssertion(request) - - when (userVerificationRequirement) { - UserVerificationRequirement.DISCOURAGED -> { - authenticateFido2Credential(request) - } - - UserVerificationRequirement.PREFERRED -> { - sendUserVerificationEvent(isRequired = false, selectedCipher = selectedCipher) - } - - UserVerificationRequirement.REQUIRED -> { - sendUserVerificationEvent(isRequired = true, selectedCipher = selectedCipher) - } - } - } - - private fun handleFido2AssertionResultReceive( - action: VaultItemListingsAction.Internal.Fido2AssertionResultReceive, - ) { - fido2CredentialManager.isUserVerified = false - clearDialogState() - sendEvent( - VaultItemListingEvent.CompleteFido2Assertion(action.result), - ) - } - private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) { mutableStateFlow.update { currentState -> currentState.copy( @@ -1505,27 +1384,6 @@ class VaultItemListingViewModel @Inject constructor( } } - /** - * Takes the given vault data and filters it for FIDO 2 credential selection. - */ - @Suppress("MaxLineLength") - private suspend fun DataState.filterForFidoGetCredentialsIfNecessary(): DataState { - val request = state.fido2GetCredentialsRequest ?: return this - return this.map { vaultData -> - val matchUri = request.origin - ?: request.packageName - .toAndroidAppUriString() - - vaultData.copy( - cipherViewList = cipherMatchingManager.filterCiphersForMatches( - ciphers = vaultData.cipherViewList, - matchUri = matchUri, - ), - fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(), - ) - } - } - /** * Takes the given vault data and filters it for totp data. */ @@ -2404,20 +2262,6 @@ sealed class VaultItemListingsAction { data class Fido2RegisterCredentialResultReceive( val result: Fido2RegisterCredentialResult, ) : Internal() - - /** - * Indicates that FIDO 2 assertion request data has been received. - */ - data class Fido2AssertionDataReceive( - val data: Fido2CredentialAssertionRequest, - ) : Internal() - - /** - * Indicates that a result of a FIDO 2 credential assertion has been received. - */ - data class Fido2AssertionResultReceive( - val result: Fido2CredentialAssertionResult, - ) : Internal() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt index 2818ff4ab..b712b0018 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt @@ -31,12 +31,14 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState +import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2Credential import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager @@ -75,6 +77,8 @@ class Fido2ProviderProcessorTest { private val vaultRepository: VaultRepository = mockk { every { ciphersStateFlow } returns mutableCiphersStateFlow } + private val environmentRepository: EnvironmentRepository = mockk { + } private val passkeyAssertionOptions = createMockPasskeyAssertionOptions(number = 1) private val fido2CredentialManager: Fido2CredentialManager = mockk() private val fido2CredentialStore: Fido2CredentialStore = mockk() @@ -294,11 +298,11 @@ class Fido2ProviderProcessorTest { ) every { cancellationSignal.setOnCancelListener(any()) } just runs every { callback.onResult(capture(captureSlot)) } just runs - every { context.getString(any()) } returns "mockTitle" + every { context.getString(any(), any()) } returns "mockTitle" every { intentManager.createFido2UnlockPendingIntent( action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT", - userId = "mockUserId-1", + userId = DEFAULT_USER_STATE.activeUserId, requestCode = any(), ) } returns mockIntent @@ -315,7 +319,7 @@ class Fido2ProviderProcessorTest { callback.onResult(any()) intentManager.createFido2UnlockPendingIntent( action = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT", - userId = "mockUserId-1", + userId = DEFAULT_USER_STATE.activeUserId, requestCode = any(), ) } @@ -339,8 +343,16 @@ class Fido2ProviderProcessorTest { val callback: OutcomeReceiver = mockk() val captureSlot = slot() mutableUserStateFlow.value = DEFAULT_USER_STATE + every { context.getString(any(), any()) } returns "mockEmail-0" every { cancellationSignal.setOnCancelListener(any()) } just runs every { callback.onError(capture(captureSlot)) } just runs + every { + intentManager.createFido2UnlockPendingIntent( + action = UNLOCK_ACCOUNT_INTENT, + userId = "mockUserId-0", + requestCode = 0, + ) + } returns mockk() fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback) @@ -365,8 +377,16 @@ class Fido2ProviderProcessorTest { val callback: OutcomeReceiver = mockk() val captureSlot = slot() mutableUserStateFlow.value = DEFAULT_USER_STATE + every { context.getString(any(), any()) } returns "mockEmail-0" every { cancellationSignal.setOnCancelListener(any()) } just runs every { callback.onError(capture(captureSlot)) } just runs + every { + intentManager.createFido2UnlockPendingIntent( + action = UNLOCK_ACCOUNT_INTENT, + userId = "mockUserId-0", + requestCode = 0, + ) + } returns mockk() fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback) @@ -393,8 +413,16 @@ class Fido2ProviderProcessorTest { val mockCipherViews = listOf(createMockCipherView(number = 1)) mutableUserStateFlow.value = DEFAULT_USER_STATE mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews) + every { context.getString(any(), any()) } returns "mockEmail-0" every { cancellationSignal.setOnCancelListener(any()) } just runs every { callback.onError(capture(captureSlot)) } just runs + every { + intentManager.createFido2UnlockPendingIntent( + action = UNLOCK_ACCOUNT_INTENT, + userId = "mockUserId-0", + requestCode = 0, + ) + } returns mockk() coEvery { vaultRepository.getDecryptedFido2CredentialAutofillViews(any()) } returns DecryptFido2CredentialAutofillViewResult.Error @@ -426,6 +454,10 @@ class Fido2ProviderProcessorTest { @Suppress("MaxLineLength") @Test fun `processGetCredentialRequest should invoke callback with filtered and discovered passkeys`() { + val userState = UserState( + activeUserId = "mockUserId-0", + accounts = createMockAccounts(1), + ) val mockOption = BeginGetPublicKeyCredentialOption( candidateQueryData = Bundle(), id = "", @@ -436,19 +468,28 @@ class Fido2ProviderProcessorTest { } val callback: OutcomeReceiver = mockk() val captureSlot = slot() - val mockCipherViews = listOf(createMockCipherView(number = 1)) + val mockCipherView = createMockCipherView( + number = 1, + fido2Credentials = listOf(createMockSdkFido2Credential(number = 1)), + ) + val mockCipherViews = listOf(mockCipherView) val mockFido2CredentialAutofillViews = listOf( - createMockFido2CredentialAutofillView(number = 1), + createMockFido2CredentialAutofillView( + number = 1, + cipherId = mockCipherView.id, + rpId = "mockRelyingPartyId-1", + ), ) val mockIntent: PendingIntent = mockk() val mockPublicKeyCredentialEntry: PublicKeyCredentialEntry = mockk() - mutableUserStateFlow.value = DEFAULT_USER_STATE + mutableUserStateFlow.value = userState mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews) every { cancellationSignal.setOnCancelListener(any()) } just runs + every { context.getString(any(), any()) } returns "mockEmail-0" every { callback.onResult(capture(captureSlot)) } just runs coEvery { vaultRepository.silentlyDiscoverCredentials( - userId = DEFAULT_USER_STATE.activeUserId, + userId = userState.activeUserId, fido2CredentialStore = fido2CredentialStore, relyingPartyId = "mockRelyingPartyId-1", ) @@ -458,8 +499,8 @@ class Fido2ProviderProcessorTest { } returns DecryptFido2CredentialAutofillViewResult.Success(mockFido2CredentialAutofillViews) every { intentManager.createFido2GetCredentialPendingIntent( - action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY", - userId = DEFAULT_USER_STATE.activeUserId, + action = GET_PASSKEY_INTENT, + userId = userState.activeUserId, credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(), cipherId = mockFido2CredentialAutofillViews.first().cipherId, requestCode = any(), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt index d54c6f305..8a4991d40 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/fido2/manager/Fido2CompletionManagerTest.kt @@ -6,12 +6,10 @@ import android.content.Intent import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PublicKeyCredentialEntry +import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult -import com.x8bit.bitwarden.data.autofill.fido2.processor.GET_PASSKEY_INTENT -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView -import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import io.mockk.Called import io.mockk.MockKVerificationScope import io.mockk.Ordering @@ -84,8 +82,6 @@ class Fido2CompletionManagerTest { @Nested inner class DefaultImplementation { - private val mockIntentManager = mockk() - @BeforeEach fun setUp() { fido2CompletionManager = Fido2CompletionManagerImpl(mockActivity) @@ -189,20 +185,10 @@ class Fido2CompletionManagerTest { mockkStatic(PendingIntent::class) val mockCredentialEntry = mockk() - val mockFido2AutofillView = createMockFido2CredentialAutofillView(number = 1) every { anyConstructed().build() } returns mockCredentialEntry - every { - mockIntentManager.createFido2GetCredentialPendingIntent( - action = GET_PASSKEY_INTENT, - userId = "mockUserId", - credentialId = mockFido2AutofillView.credentialId.toString(), - cipherId = mockFido2AutofillView.cipherId, - requestCode = any(), - ) - } returns mockk() every { mockActivity.getString(any()) } returns "No username" fido2CompletionManager @@ -230,6 +216,42 @@ class Fido2CompletionManagerTest { assertTrue(responseSlot.captured.authenticationActions.isEmpty()) } + @Suppress("MaxLineLength") + @Test + fun `completeFido2GetCredentials should set username to default value when userNameForUi is null`() { + mockkConstructor(PublicKeyCredentialEntry.Builder::class) + mockkStatic(PendingIntent::class) + val mockCredentialEntry = mockk() + every { + anyConstructed().build() + } returns mockCredentialEntry + every { mockActivity.getString(any()) } returns "No Username" + + fido2CompletionManager + .completeFido2GetCredentialRequest( + Fido2GetCredentialsResult.Success( + userId = "mockUserId", + options = mockk(), + credentialEntries = listOf(mockCredentialEntry), + ), + ) + + val responseSlot = slot() + verify { + mockActivity.getString(R.string.no_username) + anyConstructed().build() + PendingIntentHandler.setBeginGetCredentialResponse( + intent = any(), + response = capture(responseSlot), + ) + } + + assertEquals( + listOf(mockCredentialEntry), + responseSlot.captured.credentialEntries, + ) + } + @Suppress("MaxLineLength") @Test fun `completeFido2GetCredentials should set GetCredentialException, set activity result, then finish activity when result is Error`() {