[PM-8137] Refactor FIDO 2 credential registration result object (#3445)

This commit is contained in:
Patrick Honkonen 2024-07-11 18:04:24 -04:00 committed by GitHub
parent c409132825
commit 9b240ddf5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 270 additions and 91 deletions

View file

@ -2,12 +2,12 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest 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.Fido2ValidateOriginResult
/** /**
* Responsible for managing FIDO 2 credential creation and authentication. * Responsible for managing FIDO 2 credential registration and authentication.
*/ */
interface Fido2CredentialManager { interface Fido2CredentialManager {
@ -32,5 +32,5 @@ interface Fido2CredentialManager {
userId: String, userId: String,
fido2CredentialRequest: Fido2CredentialRequest, fido2CredentialRequest: Fido2CredentialRequest,
selectedCipherView: CipherView, selectedCipherView: CipherView,
): Fido2CreateCredentialResult ): Fido2RegisterCredentialResult
} }

View file

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.autofill.fido2.manager package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.fido.ClientData import com.bitwarden.fido.ClientData
import com.bitwarden.sdk.CheckUserResult import com.bitwarden.sdk.CheckUserResult
@ -10,8 +9,8 @@ import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest 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.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.platform.manager.AssetManager import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asFailure
@ -46,15 +45,11 @@ class Fido2CredentialManagerImpl(
userId: String, userId: String,
fido2CredentialRequest: Fido2CredentialRequest, fido2CredentialRequest: Fido2CredentialRequest,
selectedCipherView: CipherView, selectedCipherView: CipherView,
): Fido2CreateCredentialResult { ): Fido2RegisterCredentialResult {
val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) { val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) {
fido2CredentialRequest.callingAppInfo.getAppSigningSignatureFingerprint() fido2CredentialRequest.callingAppInfo.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) } ?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2CreateCredentialResult.Error( ?: return Fido2RegisterCredentialResult.Error
exception = CreateCredentialUnknownException(
errorMessage = "Application contains multiple signing certificates.",
),
)
} else { } else {
ClientData.DefaultWithExtraData( ClientData.DefaultWithExtraData(
androidPackageName = fido2CredentialRequest androidPackageName = fido2CredentialRequest
@ -82,9 +77,9 @@ class Fido2CredentialManagerImpl(
.map { it.toAndroidAttestationResponse() } .map { it.toAndroidAttestationResponse() }
.mapCatching { json.encodeToString(it) } .mapCatching { json.encodeToString(it) }
.fold( .fold(
onSuccess = { Fido2CreateCredentialResult.Success(it) }, onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = { onFailure = {
Fido2CreateCredentialResult.Error(CreateCredentialUnknownException()) Fido2RegisterCredentialResult.Error
}, },
) )
} }

View file

@ -1,23 +0,0 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import androidx.credentials.exceptions.CreateCredentialException
/**
* Models the data returned from creating a FIDO 2 credential.
*/
sealed class Fido2CreateCredentialResult {
/**
* Models a successful response for creating a credential.
*/
data class Success(
val registrationResponse: String,
) : Fido2CreateCredentialResult()
/**
* Models an error response for creating a credential.
*/
data class Error(
val exception: CreateCredentialException,
) : Fido2CreateCredentialResult()
}

View file

@ -0,0 +1,24 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
/**
* Models the data returned from creating a FIDO 2 credential.
*/
sealed class Fido2RegisterCredentialResult {
/**
* Indicates the credential has been successfully registered.
*/
data class Success(
val registrationResponse: String,
) : Fido2RegisterCredentialResult()
/**
* Indicates there was an error and the credential was not registered.
*/
data object Error : Fido2RegisterCredentialResult()
/**
* Indicates the user cancelled the request.
*/
data object Cancelled : Fido2RegisterCredentialResult()
}

View file

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.ui.autofill.fido2.manager package com.x8bit.bitwarden.ui.autofill.fido2.manager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
/** /**
* A manager for completing the FIDO 2 creation process. * A manager for completing the FIDO 2 creation process.
@ -8,7 +8,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
interface Fido2CompletionManager { interface Fido2CompletionManager {
/** /**
* Completes the FIDO 2 creation process with the provided [result]. * Completes the FIDO 2 registration process with the provided [result].
*/ */
fun completeFido2Create(result: Fido2CreateCredentialResult) fun completeFido2Registration(result: Fido2RegisterCredentialResult)
} }

View file

@ -5,8 +5,10 @@ import android.content.Intent
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.credentials.CreatePublicKeyCredentialResponse import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/** /**
@ -18,19 +20,19 @@ class Fido2CompletionManagerImpl(
) : Fido2CompletionManager { ) : Fido2CompletionManager {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun completeFido2Create(result: Fido2CreateCredentialResult) { override fun completeFido2Registration(result: Fido2RegisterCredentialResult) {
activity.also { activity.also {
val intent = Intent() val intent = Intent()
when (result) { when (result) {
is Fido2CreateCredentialResult.Error -> { is Fido2RegisterCredentialResult.Error -> {
PendingIntentHandler PendingIntentHandler
.setCreateCredentialException( .setCreateCredentialException(
intent = intent, intent = intent,
exception = result.exception, exception = CreateCredentialUnknownException(),
) )
} }
is Fido2CreateCredentialResult.Success -> { is Fido2RegisterCredentialResult.Success -> {
PendingIntentHandler PendingIntentHandler
.setCreateCredentialResponse( .setCreateCredentialResponse(
intent = intent, intent = intent,
@ -39,6 +41,14 @@ class Fido2CompletionManagerImpl(
), ),
) )
} }
is Fido2RegisterCredentialResult.Cancelled -> {
PendingIntentHandler
.setCreateCredentialException(
intent = intent,
exception = CreateCredentialCancellationException(),
)
}
} }
it.setResult(Activity.RESULT_OK, intent) it.setResult(Activity.RESULT_OK, intent)
it.finish() it.finish()

View file

@ -112,8 +112,8 @@ fun VaultAddEditScreen(
) )
} }
is VaultAddEditEvent.CompleteFido2Create -> { is VaultAddEditEvent.CompleteFido2Registration -> {
fido2CompletionManager.completeFido2Create(event.result) fido2CompletionManager.completeFido2Registration(event.result)
} }
} }
} }

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit package com.x8bit.bitwarden.ui.vault.feature.addedit
import android.os.Parcelable import android.os.Parcelable
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
@ -10,8 +9,8 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@ -379,14 +378,12 @@ class VaultAddEditViewModel @Inject constructor(
?: run { ?: run {
sendAction( sendAction(
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive( VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
result = Fido2CreateCredentialResult.Error( result = Fido2RegisterCredentialResult.Error,
exception = CreateCredentialUnknownException(),
),
), ),
) )
return@launch return@launch
} }
val result: Fido2CreateCredentialResult = val result: Fido2RegisterCredentialResult =
fido2CredentialManager.registerFido2Credential( fido2CredentialManager.registerFido2Credential(
userId = activeUserId, userId = activeUserId,
fido2CredentialRequest = request, fido2CredentialRequest = request,
@ -1356,15 +1353,19 @@ class VaultAddEditViewModel @Inject constructor(
) { ) {
clearDialogState() clearDialogState()
when (action.result) { when (action.result) {
is Fido2CreateCredentialResult.Error -> { is Fido2RegisterCredentialResult.Error -> {
sendEvent(VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText())) sendEvent(VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText()))
} }
is Fido2CreateCredentialResult.Success -> { is Fido2RegisterCredentialResult.Success -> {
sendEvent(VaultAddEditEvent.ShowToast(R.string.item_updated.asText())) sendEvent(VaultAddEditEvent.ShowToast(R.string.item_updated.asText()))
} }
Fido2RegisterCredentialResult.Cancelled -> {
// no-op: The OS will handle re-displaying the system prompt.
} }
sendEvent(VaultAddEditEvent.CompleteFido2Create(action.result)) }
sendEvent(VaultAddEditEvent.CompleteFido2Registration(action.result))
} }
//endregion Internal Type Handlers //endregion Internal Type Handlers
@ -1989,12 +1990,12 @@ sealed class VaultAddEditEvent {
) : VaultAddEditEvent() ) : VaultAddEditEvent()
/** /**
* Complete the current FIDO 2 credential creation process. * Complete the current FIDO 2 credential registration process.
* *
* @property result the result of FIDO 2 credential creation. * @property result the result of FIDO 2 credential creation.
*/ */
data class CompleteFido2Create( data class CompleteFido2Registration(
val result: Fido2CreateCredentialResult, val result: Fido2RegisterCredentialResult,
) : VaultAddEditEvent() ) : VaultAddEditEvent()
} }
@ -2484,7 +2485,7 @@ sealed class VaultAddEditAction {
* Indicates that the FIDO 2 registration result has been received. * Indicates that the FIDO 2 registration result has been received.
*/ */
data class Fido2RegisterCredentialResultReceive( data class Fido2RegisterCredentialResultReceive(
val result: Fido2CreateCredentialResult, val result: Fido2RegisterCredentialResult,
) : Internal() ) : Internal()
} }
} }

View file

@ -134,8 +134,8 @@ fun VaultItemListingScreen(
onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId)) onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId))
} }
is VaultItemListingEvent.CompleteFido2Create -> { is VaultItemListingEvent.CompleteFido2Registration -> {
fido2CompletionManager.completeFido2Create(event.result) fido2CompletionManager.completeFido2Registration(event.result)
} }
} }
} }

View file

@ -1,15 +1,14 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.os.Parcelable import android.os.Parcelable
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest 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.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
@ -408,10 +407,8 @@ class VaultItemListingViewModel @Inject constructor(
private fun handleDismissFido2ErrorDialogClick() { private fun handleDismissFido2ErrorDialogClick() {
sendEvent( sendEvent(
VaultItemListingEvent.CompleteFido2Create( VaultItemListingEvent.CompleteFido2Registration(
result = Fido2CreateCredentialResult.Error( result = Fido2RegisterCredentialResult.Error,
exception = CreateCredentialUnknownException(),
),
), ),
) )
} }
@ -766,15 +763,19 @@ class VaultItemListingViewModel @Inject constructor(
) { ) {
mutableStateFlow.update { it.copy(dialogState = null) } mutableStateFlow.update { it.copy(dialogState = null) }
when (action.result) { when (action.result) {
is Fido2CreateCredentialResult.Error -> { is Fido2RegisterCredentialResult.Error -> {
sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText())) sendEvent(VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()))
} }
is Fido2CreateCredentialResult.Success -> { is Fido2RegisterCredentialResult.Success -> {
sendEvent(VaultItemListingEvent.ShowToast(R.string.item_updated.asText())) sendEvent(VaultItemListingEvent.ShowToast(R.string.item_updated.asText()))
} }
Fido2RegisterCredentialResult.Cancelled -> {
// no-op: The OS will handle re-displaying the system prompt.
} }
sendEvent(VaultItemListingEvent.CompleteFido2Create(action.result)) }
sendEvent(VaultItemListingEvent.CompleteFido2Registration(action.result))
} }
private fun handleValidateFido2OriginResultReceive( private fun handleValidateFido2OriginResultReceive(
@ -1346,8 +1347,8 @@ sealed class VaultItemListingEvent {
* *
* @property result the result of FIDO 2 credential creation. * @property result the result of FIDO 2 credential creation.
*/ */
data class CompleteFido2Create( data class CompleteFido2Registration(
val result: Fido2CreateCredentialResult, val result: Fido2RegisterCredentialResult,
) : VaultItemListingEvent() ) : VaultItemListingEvent()
} }
@ -1529,7 +1530,7 @@ sealed class VaultItemListingsAction {
* Indicates that a result for FIDO 2 credential registration has been received. * Indicates that a result for FIDO 2 credential registration has been received.
*/ */
data class Fido2RegisterCredentialResultReceive( data class Fido2RegisterCredentialResultReceive(
val result: Fido2CreateCredentialResult, val result: Fido2RegisterCredentialResult,
) : Internal() ) : Internal()
} }
} }

View file

@ -10,8 +10,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalA
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2AttestationResponse
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest 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.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
import com.x8bit.bitwarden.data.platform.manager.AssetManager import com.x8bit.bitwarden.data.platform.manager.AssetManager
@ -486,7 +486,7 @@ class Fido2CredentialManagerTest {
) )
assertTrue( assertTrue(
result is Fido2CreateCredentialResult.Error, result is Fido2RegisterCredentialResult.Error,
) )
} }
@ -530,7 +530,7 @@ class Fido2CredentialManagerTest {
) )
assertTrue( assertTrue(
result is Fido2CreateCredentialResult.Error, result is Fido2RegisterCredentialResult.Error,
) )
} }
} }

View file

@ -33,7 +33,7 @@ import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import androidx.core.net.toUri import androidx.core.net.toUri
import com.bitwarden.vault.UriMatchType import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
@ -96,7 +96,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
every { launchUri(any()) } just runs every { launchUri(any()) } just runs
} }
private val fido2CompletionManager: Fido2CompletionManager = mockk { private val fido2CompletionManager: Fido2CompletionManager = mockk {
every { completeFido2Create(any()) } just runs every { completeFido2Registration(any()) } just runs
} }
@Before @Before
@ -194,11 +194,11 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Test @Test
fun `on CompleteFido2Create even should invoke Fido2CompletionManager`() { fun `on CompleteFido2Create even should invoke Fido2CompletionManager`() {
val result = Fido2CreateCredentialResult.Success( val result = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockRegistrationResponse", registrationResponse = "mockRegistrationResponse",
) )
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Create(result = result)) mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Registration(result = result))
verify { fido2CompletionManager.completeFido2Create(result) } verify { fido2CompletionManager.completeFido2Registration(result) }
} }
@Test @Test

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit package com.x8bit.bitwarden.ui.vault.feature.addedit
import android.content.pm.SigningInfo import android.content.pm.SigningInfo
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import app.cash.turbine.turbineScope import app.cash.turbine.turbineScope
@ -17,8 +16,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType 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.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
@ -725,7 +724,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
), ),
) )
val mockCreateResult = Fido2CreateCredentialResult.Success( val mockCreateResult = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockRegistrationResponse", registrationResponse = "mockRegistrationResponse",
) )
coEvery { coEvery {
@ -750,7 +749,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
eventTurbine.awaitItem(), eventTurbine.awaitItem(),
) )
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Create(result = mockCreateResult), VaultAddEditEvent.CompleteFido2Registration(result = mockCreateResult),
eventTurbine.awaitItem(), eventTurbine.awaitItem(),
) )
assertEquals(stateWithName, stateTurbine.awaitItem()) assertEquals(stateWithName, stateTurbine.awaitItem())
@ -767,7 +766,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in add mode during fido2, SaveClick should show dialog, register credential, show toast on error, and emit ExitApp`() = fun `in add mode during fido2, SaveClick should show dialog, register credential, show toast on error, and emit ExitApp when result is Error`() =
runTest { runTest {
val fido2CredentialRequest = Fido2CredentialRequest( val fido2CredentialRequest = Fido2CredentialRequest(
userId = "mockUserId", userId = "mockUserId",
@ -807,9 +806,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
), ),
) )
val mockCreateResult = Fido2CreateCredentialResult.Error( val mockCreateResult = Fido2RegisterCredentialResult.Error
exception = CreateCredentialUnknownException(),
)
coEvery { coEvery {
fido2CredentialManager.registerFido2Credential( fido2CredentialManager.registerFido2Credential(
userId = "mockUserId", userId = "mockUserId",
@ -832,7 +829,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
eventTurbine.awaitItem(), eventTurbine.awaitItem(),
) )
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Create(result = mockCreateResult), VaultAddEditEvent.CompleteFido2Registration(result = mockCreateResult),
eventTurbine.awaitItem(), eventTurbine.awaitItem(),
) )
assertEquals(stateWithName, stateTurbine.awaitItem()) assertEquals(stateWithName, stateTurbine.awaitItem())
@ -847,6 +844,86 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `in add mode during fido2, SaveClick should show saving dialog, register credential, dismiss dialog, show toast on error, and emit ExitApp when activeUserId is null`() =
runTest {
val fido2CredentialRequest = Fido2CredentialRequest(
userId = "mockUserId",
requestJson = "mockRequestJson",
packageName = "mockPackageName",
signingInfo = mockk<SigningInfo>(),
origin = null,
)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CredentialRequest = fido2CredentialRequest,
)
val stateWithDialog = createVaultAddItemState(
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
dialogState = VaultAddEditState.DialogState.Loading(
R.string.saving.asText(),
),
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
)
.copy(shouldExitOnSave = true)
val stateWithName = createVaultAddItemState(
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
)
.copy(shouldExitOnSave = true)
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(),
)
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
),
)
val mockCreateResult = Fido2RegisterCredentialResult.Error
coEvery {
fido2CredentialManager.registerFido2Credential(
userId = "mockUserId",
selectedCipherView = any(),
fido2CredentialRequest = fido2CredentialRequest,
)
} returns mockCreateResult
every { authRepository.activeUserId } returns null
turbineScope {
val stateTurbine = viewModel.stateFlow.testIn(backgroundScope)
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(stateWithName, stateTurbine.awaitItem())
assertEquals(stateWithDialog, stateTurbine.awaitItem())
assertEquals(
VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText()),
eventTurbine.awaitItem(),
)
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(result = mockCreateResult),
eventTurbine.awaitItem(),
)
assertEquals(stateWithName, stateTurbine.awaitItem())
coVerify(exactly = 0) {
fido2CredentialManager.registerFido2Credential(
userId = "mockUserId",
selectedCipherView = any(),
fido2CredentialRequest = fido2CredentialRequest,
)
}
}
}
@Test @Test
fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() = fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() =
runTest { runTest {

View file

@ -82,7 +82,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
every { launchUri(any()) } just runs every { launchUri(any()) } just runs
} }
private val fido2CompletionManager: Fido2CompletionManager = mockk { private val fido2CompletionManager: Fido2CompletionManager = mockk {
every { completeFido2Create(any()) } just runs every { completeFido2Registration(any()) } just runs
} }
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest 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.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
@ -66,6 +67,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Clock import java.time.Clock
@ -1736,6 +1738,98 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Error
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive(
mockResult,
),
)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.ShowToast(R.string.an_error_has_occurred.asText()),
awaitItem(),
)
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(mockResult),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Success(
registrationResponse = "mockResponse",
)
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive(
mockResult,
),
)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.ShowToast(R.string.item_updated.asText()),
awaitItem(),
)
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(mockResult),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Cancelled should emit CompleteFido2Registration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Cancelled
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive(
mockResult,
),
)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(mockResult),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 create`() =
runTest {
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.DismissFido2CreationErrorDialogClick)
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialogState)
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(
result = Fido2RegisterCredentialResult.Error,
),
awaitItem(),
)
}
}
@Suppress("CyclomaticComplexMethod") @Suppress("CyclomaticComplexMethod")
private fun createSavedStateHandleWithVaultItemListingType( private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType, vaultItemListingType: VaultItemListingType,