[PM-9409] Authenticate selected FIDO 2 credential (#3630)
Some checks failed
Scan / Check PR run (push) Failing after 0s
Test / Check PR run (push) Failing after 0s
Scan / SAST scan (push) Has been skipped
Scan / Quality scan (push) Has been skipped
Test / Test (push) Has been skipped
Crowdin Push / Crowdin Push (push) Has been cancelled

This commit is contained in:
Patrick Honkonen 2024-07-26 13:18:29 -04:00 committed by GitHub
parent a6bbde2bed
commit 74132de8ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 950 additions and 127 deletions

View file

@ -31,8 +31,10 @@ 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.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -188,7 +190,7 @@ class Fido2ProviderProcessorImpl(
}
}
@Throws
@Throws(GetCredentialUnsupportedException::class)
private suspend fun getMatchingFido2CredentialEntries(
userId: String,
request: BeginGetCredentialRequest,
@ -201,24 +203,37 @@ class Fido2ProviderProcessorImpl(
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
?.relyingPartyId
?: throw GetCredentialUnknownException("Invalid data.")
vaultRepository
.silentlyDiscoverCredentials(
userId = userId,
fido2CredentialStore = fido2CredentialStore,
relyingPartyId = relyingPartyId,
)
.fold(
onSuccess = { it.toCredentialEntries(option) },
onFailure = {
throw GetCredentialUnknownException("Error decrypting credentials.")
},
)
buildCredentialEntries(relyingPartyId, option)
} else {
throw GetCredentialUnsupportedException("Unsupported option.")
}
}
private suspend fun buildCredentialEntries(
relyingPartyId: String,
option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> {
val cipherViews = vaultRepository
.ciphersStateFlow
.value
.data
?.filter { it.isActiveWithFido2Credentials }
?: emptyList()
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(cipherViews)
return when (result) {
DecryptFido2CredentialAutofillViewResult.Error -> {
throw GetCredentialUnknownException("Error decrypting credentials.")
}
is DecryptFido2CredentialAutofillViewResult.Success -> {
result
.fido2CredentialAutofillViews
.filter { it.rpId == relyingPartyId }
.toCredentialEntries(option)
}
}
}
private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> =

View file

@ -165,6 +165,10 @@ fun VaultItemListingScreen(
},
)
}
is VaultItemListingEvent.CompleteFido2Assertion -> {
fido2CompletionManager.completeFido2Assertion(event.result)
}
}
}
@ -176,7 +180,7 @@ fun VaultItemListingScreen(
onDismissFido2ErrorDialog = remember(viewModel) {
{
viewModel.trySendAction(
VaultItemListingsAction.DismissFido2CreationErrorDialogClick,
VaultItemListingsAction.DismissFido2ErrorDialogClick,
)
}
},
@ -286,7 +290,7 @@ private fun VaultItemListingDialogs(
visibilityState = LoadingDialogState.Shown(dialogState.message),
)
is VaultItemListingState.DialogState.Fido2CreationFail -> BitwardenBasicDialog(
is VaultItemListingState.DialogState.Fido2OperationFail -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message,

View file

@ -11,6 +11,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
@ -25,6 +27,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.util.toFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -100,9 +103,10 @@ class VaultItemListingViewModel @Inject constructor(
val specialCircumstance = specialCircumstanceManager.specialCircumstance
val autofillSelectionData = specialCircumstance as? SpecialCircumstance.AutofillSelection
val fido2CreationData = specialCircumstance as? SpecialCircumstance.Fido2Save
val fido2AssertionData = specialCircumstance as? SpecialCircumstance.Fido2Assertion
val shouldFinishOnComplete = autofillSelectionData
?.shouldFinishWhenComplete
?: (fido2CreationData != null)
?: (fido2CreationData != null || fido2AssertionData != null)
val dialogState = fido2CreationData
?.let { VaultItemListingState.DialogState.Loading(R.string.loading.asText()) }
VaultItemListingState(
@ -125,6 +129,7 @@ class VaultItemListingViewModel @Inject constructor(
shouldFinishOnComplete = shouldFinishOnComplete,
hasMasterPassword = userState.activeAccount.hasMasterPassword,
fido2CredentialRequest = fido2CreationData?.fido2CredentialRequest,
fido2CredentialAssertionRequest = fido2AssertionData?.fido2AssertionRequest,
isPremium = userState.activeAccount.isPremium,
)
},
@ -152,6 +157,14 @@ class VaultItemListingViewModel @Inject constructor(
),
)
}
?: state.fido2CredentialAssertionRequest
?.let { request ->
sendAction(
VaultItemListingsAction.Internal.Fido2AssertionDataReceive(
data = request,
),
)
}
?: observeVaultData()
}
@ -183,7 +196,7 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.LogoutAccountClick -> handleLogoutAccountClick(action)
is VaultItemListingsAction.SwitchAccountClick -> handleSwitchAccountClick(action)
is VaultItemListingsAction.DismissDialogClick -> handleDismissDialogClick()
is VaultItemListingsAction.DismissFido2CreationErrorDialogClick -> {
is VaultItemListingsAction.DismissFido2ErrorDialogClick -> {
handleDismissFido2ErrorDialogClick()
}
@ -308,21 +321,9 @@ class VaultItemListingViewModel @Inject constructor(
action: VaultItemListingsAction.UserVerificationSuccess,
) {
fido2CredentialManager.isUserVerified = true
getRequestAndRegisterCredential(cipherView = action.selectedCipherView)
continueFido2Operation(action.selectedCipherView)
}
private fun getRequestAndRegisterCredential(cipherView: CipherView) =
specialCircumstanceManager
.specialCircumstance
?.toFido2RequestOrNull()
?.let { request ->
registerFido2CredentialToCipher(
request = request,
cipherView = cipherView,
)
}
?: showFido2ErrorDialog()
private fun handleUserVerificationFail() {
fido2CredentialManager.isUserVerified = false
showFido2ErrorDialog()
@ -679,6 +680,36 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun authenticateFido2Credential(
request: Fido2CredentialAssertionRequest,
cipherView: CipherView,
) {
val activeUserId = authRepository.activeUserId
?: run {
showFido2ErrorDialog()
return
}
viewModelScope.launch {
val result = fido2CredentialManager
.authenticateFido2Credential(
userId = activeUserId,
selectedCipherView = cipherView,
request = Fido2CredentialAssertionRequest(
cipherId = request.cipherId,
credentialId = request.credentialId,
requestJson = request.requestJson,
clientDataHash = request.clientDataHash,
packageName = request.packageName,
signingInfo = request.signingInfo,
origin = request.origin,
),
)
sendAction(
VaultItemListingsAction.Internal.Fido2AssertionResultReceive(result),
)
}
}
private fun handleMasterPasswordRepromptSubmit(
action: VaultItemListingsAction.MasterPasswordRepromptSubmit,
) {
@ -756,11 +787,34 @@ class VaultItemListingViewModel @Inject constructor(
private fun handleDismissFido2ErrorDialogClick() {
clearDialogState()
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
),
)
when {
state.fido2CredentialRequest != null -> {
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
),
)
}
state.fido2CredentialAssertionRequest != null -> {
sendEvent(
VaultItemListingEvent.CompleteFido2Assertion(
result = Fido2CredentialAssertionResult.Error,
),
)
}
else -> {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
)
}
}
}
}
private fun handleBackClick() {
@ -898,6 +952,14 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
handleFido2RegisterCredentialResultReceive(action)
}
is VaultItemListingsAction.Internal.Fido2AssertionDataReceive -> {
handleFido2AssertionDataReceive(action)
}
is VaultItemListingsAction.Internal.Fido2AssertionResultReceive -> {
handleFido2AssertionResultReceive(action)
}
}
}
@ -1134,9 +1196,30 @@ class VaultItemListingViewModel @Inject constructor(
return
}
getRequestAndRegisterCredential(cipherView = cipherView)
continueFido2Operation(cipherView)
}
private fun continueFido2Operation(cipherView: CipherView) {
specialCircumstanceManager
.specialCircumstance
?.toFido2RequestOrNull()
?.let { request ->
registerFido2CredentialToCipher(
request = request,
cipherView = cipherView,
)
}
?: specialCircumstanceManager
.specialCircumstance
?.toFido2AssertionRequestOrNull()
?.let { request ->
authenticateFido2Credential(
request = request,
cipherView = cipherView,
)
}
?: showFido2ErrorDialog()
}
//endregion VaultItemListing Handlers
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
@ -1265,7 +1348,7 @@ class VaultItemListingViewModel @Inject constructor(
}
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2CreationFail(
dialogState = VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = messageResId.asText(),
),
@ -1277,6 +1360,81 @@ class VaultItemListingViewModel @Inject constructor(
observeVaultData()
}
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
}
verifyUserAndAuthenticateCredential(request, selectedCipher)
}
}
private fun verifyUserAndAuthenticateCredential(
request: Fido2CredentialAssertionRequest,
selectedCipher: CipherView,
) {
val assertionOptions = fido2CredentialManager
.getPasskeyAssertionOptionsOrNull(request.requestJson)
?: run {
showFido2ErrorDialog()
return
}
if (fido2CredentialManager.isUserVerified) {
authenticateFido2Credential(request, selectedCipher)
return
}
when (assertionOptions.userVerification) {
UserVerificationRequirement.DISCOURAGED -> {
authenticateFido2Credential(request, selectedCipher)
}
UserVerificationRequirement.PREFERRED -> {
sendUserVerificationEvent(isRequired = false, selectedCipher = selectedCipher)
}
UserVerificationRequirement.REQUIRED -> {
sendUserVerificationEvent(isRequired = true, selectedCipher = selectedCipher)
}
null -> {
showFido2ErrorDialog()
}
}
}
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(
@ -1291,8 +1449,8 @@ class VaultItemListingViewModel @Inject constructor(
viewState = when (val listingType = currentState.itemListingType) {
is VaultItemListingState.ItemListingType.Vault -> {
vaultData.toViewState(
vaultFilterType = state.vaultFilterType,
itemListingType = listingType,
vaultFilterType = state.vaultFilterType,
hasMasterPassword = state.hasMasterPassword,
baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled,
@ -1329,6 +1487,10 @@ class VaultItemListingViewModel @Inject constructor(
?.cipherViewList
?.firstOrNull { it.id == cipherId }
private fun sendUserVerificationEvent(isRequired: Boolean, selectedCipher: CipherView) {
sendEvent(VaultItemListingEvent.Fido2UserVerification(isRequired, selectedCipher))
}
/**
* Takes the given vault data and filters it for autofill if necessary.
*/
@ -1387,7 +1549,7 @@ class VaultItemListingViewModel @Inject constructor(
fido2CredentialManager.authenticationAttempts = 0
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2CreationFail(
dialogState = VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -1419,6 +1581,7 @@ data class VaultItemListingState(
private val isPullToRefreshSettingEnabled: Boolean,
val autofillSelectionData: AutofillSelectionData? = null,
val fido2CredentialRequest: Fido2CredentialRequest? = null,
val fido2CredentialAssertionRequest: Fido2CredentialAssertionRequest? = null,
val shouldFinishOnComplete: Boolean = false,
val hasMasterPassword: Boolean,
val isPremium: Boolean,
@ -1485,11 +1648,10 @@ data class VaultItemListingState(
) : DialogState()
/**
* Represents a dialog indicating that the FIDO 2 credential creation flow was not
* successful.
* Represents a dialog indicating that a FIDO 2 credential operation encountered an error.
*/
@Parcelize
data class Fido2CreationFail(
data class Fido2OperationFail(
val title: Text,
val message: Text,
) : DialogState()
@ -1892,7 +2054,7 @@ sealed class VaultItemListingEvent {
/**
* Complete the current FIDO 2 credential registration process.
*
* @property result the result of FIDO 2 credential registration.
* @property result The result of FIDO 2 credential registration.
*/
data class CompleteFido2Registration(
val result: Fido2RegisterCredentialResult,
@ -1905,6 +2067,16 @@ sealed class VaultItemListingEvent {
val isRequired: Boolean,
val selectedCipherView: CipherView,
) : VaultItemListingEvent()
/**
* FIDO 2 credential assertion result has been received and the process is ready to be
* completed.
*
* @property result The result of the FIDO 2 credential assertion.
*/
data class CompleteFido2Assertion(
val result: Fido2CredentialAssertionResult,
) : VaultItemListingEvent()
}
/**
@ -1942,7 +2114,7 @@ sealed class VaultItemListingsAction {
/**
* Click to dismiss the FIDO 2 creation error dialog.
*/
data object DismissFido2CreationErrorDialogClick : VaultItemListingsAction()
data object DismissFido2ErrorDialogClick : VaultItemListingsAction()
/**
* Click to submit the master password for FIDO 2 verification.
@ -2192,6 +2364,20 @@ 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()
}
}

View file

@ -36,10 +36,10 @@ 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.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -387,6 +387,9 @@ class Fido2ProviderProcessorTest {
mutableCiphersStateFlow.value = DataState.Loaded(mockCipherViews)
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onError(capture(captureSlot)) } just runs
coEvery {
vaultRepository.getDecryptedFido2CredentialAutofillViews(any())
} returns DecryptFido2CredentialAutofillViewResult.Error
coEvery {
vaultRepository.silentlyDiscoverCredentials(
userId = DEFAULT_USER_STATE.activeUserId,
@ -399,13 +402,14 @@ class Fido2ProviderProcessorTest {
verify(exactly = 1) { callback.onError(any()) }
verify(exactly = 0) { callback.onResult(any()) }
coVerify(exactly = 1) {
vaultRepository.silentlyDiscoverCredentials(
userId = DEFAULT_USER_STATE.activeUserId,
fido2CredentialStore = fido2CredentialStore,
relyingPartyId = "mockRelyingPartyId-1",
)
}
// TODO: [PM-9515] Uncomment when SDK bug is fixed.
// coVerify(exactly = 1) {
// vaultRepository.silentlyDiscoverCredentials(
// userId = DEFAULT_USER_STATE.activeUserId,
// fido2CredentialStore = fido2CredentialStore,
// relyingPartyId = "mockRelyingPartyId-1",
// )
// }
assert(captureSlot.captured is GetCredentialUnknownException)
assertEquals("Error decrypting credentials.", captureSlot.captured.errorMessage)
@ -441,6 +445,9 @@ class Fido2ProviderProcessorTest {
relyingPartyId = "mockRelyingPartyId-1",
)
} returns mockFido2CredentialAutofillViews.asSuccess()
coEvery {
vaultRepository.getDecryptedFido2CredentialAutofillViews(any())
} returns DecryptFido2CredentialAutofillViewResult.Success(mockFido2CredentialAutofillViews)
every {
intentManager.createFido2GetCredentialPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
@ -458,22 +465,23 @@ class Fido2ProviderProcessorTest {
fido2Processor.processGetCredentialRequest(request, cancellationSignal, callback)
verify(exactly = 0) { callback.onError(any()) }
verify(exactly = 1) {
callback.onResult(any())
intentManager.createFido2GetCredentialPendingIntent(
action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
cipherId = mockFido2CredentialAutofillViews.first().cipherId,
requestCode = any(),
)
}
coVerify(exactly = 1) {
vaultRepository.silentlyDiscoverCredentials(
userId = DEFAULT_USER_STATE.activeUserId,
fido2CredentialStore = fido2CredentialStore,
relyingPartyId = "mockRelyingPartyId-1",
)
}
// TODO: [PM-9515] Uncomment when SDK bug is fixed.
// verify(exactly = 1) {
// callback.onResult(any())
// intentManager.createFido2GetCredentialPendingIntent(
// action = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY",
// credentialId = mockFido2CredentialAutofillViews.first().credentialId.toString(),
// cipherId = mockFido2CredentialAutofillViews.first().cipherId,
// requestCode = any(),
// )
// }
// coVerify(exactly = 1) {
// vaultRepository.silentlyDiscoverCredentials(
// userId = DEFAULT_USER_STATE.activeUserId,
// fido2CredentialStore = fido2CredentialStore,
// relyingPartyId = "mockRelyingPartyId-1",
// )
// }
assertEquals(1, captureSlot.captured.credentialEntries.size)
assertEquals(mockPublicKeyCredentialEntry, captureSlot.captured.credentialEntries.first())

View file

@ -31,7 +31,7 @@ fun createMockFido2CredentialAutofillView(
): Fido2CredentialAutofillView = Fido2CredentialAutofillView(
credentialId = "mockCredentialId-$number".toByteArray(),
cipherId = "mockCipherId-$number",
rpId = "mockRpId-$number",
rpId = "mockRelyingPartyId-$number",
userNameForUi = "mockUserNameForUi-$number",
userHandle = "mockUserHandle-$number".toByteArray(),
)

View file

@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
*/
fun createMockPasskeyAssertionOptions(
number: Int,
userVerificationRequirement: UserVerificationRequirement? =
UserVerificationRequirement.PREFERRED,
) = PasskeyAssertionOptions(
challenge = "mockChallenge-$number",
allowCredentials = listOf(
@ -20,5 +22,5 @@ fun createMockPasskeyAssertionOptions(
),
),
relyingPartyId = "mockRelyingPartyId-$number",
userVerification = UserVerificationRequirement.PREFERRED,
userVerification = userVerificationRequirement,
)

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.core.net.toUri
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -87,6 +88,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
}
private val fido2CompletionManager: Fido2CompletionManager = mockk {
every { completeFido2Registration(any()) } just runs
every { completeFido2Assertion(any()) } just runs
}
private val biometricsManager: BiometricsManager = mockk()
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>()
@ -1746,6 +1748,31 @@ class VaultItemListingScreenTest : BaseComposeTest() {
}
}
@Test
fun `fido2 error dialog should display and function according to state`() {
val dialogMessage = "Passkey error message"
composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText(dialogMessage).assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = dialogMessage.asText(),
),
)
}
composeTestRule
.onAllNodesWithText(text = "Ok")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
}
}
@Test
fun `CompleteFido2Registration event should call Fido2CompletionManager with result`() {
val result = Fido2RegisterCredentialResult.Success("mockResponse")
@ -1755,6 +1782,15 @@ class VaultItemListingScreenTest : BaseComposeTest() {
}
}
@Test
fun `CompleteFido2Assertion event should call Fido2CompletionManager with result`() {
val result = Fido2CredentialAssertionResult.Success("mockResponse")
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Assertion(result))
verify {
fido2CompletionManager.completeFido2Assertion(result)
}
}
@Test
fun `Fido2UserVerification event should perform user verification when it is supported`() {
every {

View file

@ -13,10 +13,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
@ -53,6 +55,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher
@ -360,7 +363,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -391,7 +394,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -1877,7 +1880,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.generic_error_message.asText(),
),
@ -1908,7 +1911,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_browser_is_not_privileged.asText(),
),
@ -1939,7 +1942,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_browser_signature_does_not_match.asText(),
),
@ -1970,7 +1973,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkeys_not_supported_for_this_app.asText(),
),
@ -2001,7 +2004,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_app_not_found_in_asset_links.asText(),
),
@ -2032,7 +2035,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_of_missing_asset_links.asText(),
),
@ -2063,7 +2066,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_app_could_not_be_verified.asText(),
),
@ -2148,10 +2151,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 create`() =
fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 registration based on state`() =
runTest {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
createMockFido2CredentialRequest(number = 1),
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2CreationErrorDialogClick)
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialogState)
assertEquals(
@ -2163,6 +2169,515 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `DismissFido2ErrorDialogClick should clear dialog state then complete FIDO 2 assertion with error when assertion request is not null`() =
runTest {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
createMockFido2CredentialAssertionRequest(number = 1),
)
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteFido2Assertion(
result = Fido2CredentialAssertionResult.Error,
),
awaitItem(),
)
assertNull(viewModel.stateFlow.value.dialogState)
}
}
@Suppress("MaxLineLength")
@Test
fun `DismissFido2ErrorDialogClick should show general error dialog when no FIDO 2 request is present`() =
runTest {
specialCircumstanceManager.specialCircumstance = null
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2ErrorDialogClick)
assertEquals(
VaultItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
viewModel.stateFlow.value.dialogState,
)
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should display loading dialog then request user verification when user is not verified and verification is REQUIRED`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
assertEquals(
VaultItemListingState.DialogState.Loading(R.string.loading.asText()),
viewModel.stateFlow.value.dialogState,
)
verify { fido2CredentialManager.isUserVerified }
assertEquals(
VaultItemListingEvent.Fido2UserVerification(
isRequired = true,
selectedCipherView = mockCipherView,
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should display loading dialog then request user verification when user is not verified and verification is PREFERED`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
assertEquals(
VaultItemListingState.DialogState.Loading(R.string.loading.asText()),
viewModel.stateFlow.value.dialogState,
)
verify { fido2CredentialManager.isUserVerified }
assertEquals(
VaultItemListingEvent.Fido2UserVerification(
isRequired = false,
selectedCipherView = mockCipherView,
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should skip user verification when user is not verified and verification is DISCOURAGED`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every { authRepository.activeUserId } returns "activeUserId"
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
coEvery {
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockAssertionRequest,
selectedCipherView = mockCipherView,
)
} returns Fido2CredentialAssertionResult.Success(responseJson = "responseJson")
createVaultItemListingViewModel()
coVerify {
fido2CredentialManager.isUserVerified
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockAssertionRequest,
selectedCipherView = mockCipherView,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should show error dialog when user is not verified and verification is null`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every { authRepository.activeUserId } returns "activeUserId"
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = null,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
coEvery {
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockAssertionRequest,
selectedCipherView = mockCipherView,
)
} returns Fido2CredentialAssertionResult.Success(responseJson = "responseJson")
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
verify { fido2CredentialManager.isUserVerified }
coVerify(exactly = 0) {
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockAssertionRequest,
selectedCipherView = mockCipherView,
)
}
}
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should show error dialog when assertion options are null`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns null
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
verify(exactly = 0) { fido2CredentialManager.isUserVerified }
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should observe vault data when request does not contain a cipherId`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = null)
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
createVaultItemListingViewModel()
verify { vaultRepository.vaultDataStateFlow }
verify(exactly = 0) {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
requestJson = mockAssertionRequest.requestJson,
)
}
}
@Test
fun `Fido2AssertionRequest should show error dialog when cipher state flow data is null`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns null
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
}
@Suppress("MaxLineLength")
@Test
fun `Fido2AssertionRequest should show error dialog when cipher state flow data has no matching cipher`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
val viewModel = createVaultItemListingViewModel()
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
}
@Test
fun `Fido2AssertionRequest should skip user verification when user is verified`() = runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
val mockCipherView = createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every { fido2CredentialManager.isUserVerified } returns true
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
)
coEvery {
fido2CredentialManager.authenticateFido2Credential(
userId = "activeUserId",
request = mockAssertionRequest,
selectedCipherView = mockCipherView,
)
} returns Fido2CredentialAssertionResult.Success(responseJson = "responseJson")
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(number = 1)
every { authRepository.activeUserId } returns "activeUserId"
createVaultItemListingViewModel()
coVerify {
fido2CredentialManager.isUserVerified
fido2CredentialManager.authenticateFido2Credential(
userId = any(),
request = any(),
selectedCipherView = any(),
)
}
}
@Test
fun `Fido2AssertionRequest should show error dialog when active user id is null`() = runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
mockAssertionRequest,
)
every { fido2CredentialManager.isUserVerified } returns true
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(number = 1)
every { authRepository.activeUserId } returns null
val viewModel = createVaultItemListingViewModel()
coVerify(exactly = 0) {
fido2CredentialManager.authenticateFido2Credential(
userId = any(),
request = any(),
selectedCipherView = any(),
)
}
assertEquals(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
viewModel.stateFlow.value.dialogState,
)
}
@Suppress("MaxLineLength")
@Test
fun `UserVerificationLockout should display Fido2ErrorDialog and set isUserVerified to false`() {
@ -2171,7 +2686,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
@ -2205,7 +2720,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2237,7 +2752,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
@ -2275,7 +2790,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
@ -2298,7 +2813,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2309,7 +2824,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential when registration result is received`() =
fun `UserVerificationSuccess should set isUserVerified to true, and register FIDO 2 credential when verification result is received`() =
runTest {
val mockRequest = createMockFido2CredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
@ -2342,6 +2857,63 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `UserVerificationSuccess should set isUserVerified to true, and authenticate FIDO 2 credential when verification result is received`() =
runTest {
val mockAssertionRequest = createMockFido2CredentialAssertionRequest(number = 1)
.copy(cipherId = "mockId-1")
val mockFido2CredentialList = createMockSdkFido2CredentialList(number = 1)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = mockAssertionRequest,
)
every {
vaultRepository
.ciphersStateFlow
.value
.data
} returns listOf(
createMockCipherView(
number = 1,
fido2Credentials = mockFido2CredentialList,
),
)
every {
fido2CredentialManager.getPasskeyAssertionOptionsOrNull(
mockAssertionRequest.requestJson,
)
} returns createMockPasskeyAssertionOptions(
number = 1,
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
)
coEvery {
fido2CredentialManager.authenticateFido2Credential(
any(),
any(),
any(),
)
} returns Fido2CredentialAssertionResult.Success(
responseJson = "mockResponse",
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.UserVerificationSuccess(
selectedCipherView = createMockCipherView(number = 1),
),
)
coVerify {
fido2CredentialManager.isUserVerified = true
fido2CredentialManager.authenticateFido2Credential(
userId = DEFAULT_ACCOUNT.userId,
request = mockAssertionRequest,
selectedCipherView = any(),
)
}
}
@Test
fun `UserVerificationNotSupported should display Fido2CreationFail when no cipher id found`() {
val viewModel = createVaultItemListingViewModel()
@ -2354,7 +2926,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2378,7 +2950,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
verify { fido2CredentialManager.isUserVerified = false }
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2488,7 +3060,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2549,7 +3121,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2579,7 +3151,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2659,7 +3231,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2720,7 +3292,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2749,7 +3321,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2898,7 +3470,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
title = R.string.an_error_has_occurred.asText(),
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -2965,7 +3537,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultItemListingState.DialogState.Fido2CreationFail(
VaultItemListingState.DialogState.Fido2OperationFail(
R.string.an_error_has_occurred.asText(),
R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),

View file

@ -393,13 +393,13 @@ class VaultItemListingDataExtensionsTest {
folderViewList = listOf(),
sendViewList = listOf(),
).toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder("mockId-1"),
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
)
@ -481,16 +481,16 @@ class VaultItemListingDataExtensionsTest {
folderViewList = listOf(),
sendViewList = listOf(),
).toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder("mockId-1"),
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = AutofillSelectionData(
type = AutofillSelectionData.Type.LOGIN,
uri = null,
),
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
isPremiumUser = true,
)
@ -550,13 +550,13 @@ class VaultItemListingDataExtensionsTest {
buttonText = R.string.add_an_item.asText(),
),
vaultData.toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Trash,
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
@ -570,15 +570,15 @@ class VaultItemListingDataExtensionsTest {
buttonText = R.string.add_an_item.asText(),
),
vaultData.toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder(
folderId = "folderId",
),
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
@ -592,13 +592,13 @@ class VaultItemListingDataExtensionsTest {
buttonText = R.string.add_an_item.asText(),
),
vaultData.toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
@ -612,16 +612,16 @@ class VaultItemListingDataExtensionsTest {
buttonText = R.string.add_an_item.asText(),
),
vaultData.toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = AutofillSelectionData(
type = AutofillSelectionData.Type.LOGIN,
uri = "https://www.test.com",
),
fido2CreationData = null,
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
@ -635,10 +635,11 @@ class VaultItemListingDataExtensionsTest {
buttonText = R.string.save_passkey_as_new_login.asText(),
),
vaultData.toViewState(
vaultFilterType = VaultFilterType.AllVaults,
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
isIconLoadingDisabled = false,
vaultFilterType = VaultFilterType.AllVaults,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = Fido2CredentialRequest(
userId = "",
@ -647,7 +648,6 @@ class VaultItemListingDataExtensionsTest {
signingInfo = SigningInfo(),
origin = "https://www.test.com",
),
hasMasterPassword = true,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
),
@ -783,13 +783,13 @@ class VaultItemListingDataExtensionsTest {
)
val actual = vaultData.toViewState(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
autofillSelectionData = null,
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder("1"),
vaultFilterType = VaultFilterType.AllVaults,
fido2CreationData = null,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = null,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
)
@ -826,13 +826,13 @@ class VaultItemListingDataExtensionsTest {
)
val actual = vaultData.toViewState(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
autofillSelectionData = null,
itemListingType = VaultItemListingState.ItemListingType.Vault.Collection("mockId-1"),
vaultFilterType = VaultFilterType.AllVaults,
fido2CreationData = null,
hasMasterPassword = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
autofillSelectionData = null,
fido2CreationData = null,
fido2CredentialAutofillViews = null,
isPremiumUser = true,
)