Refactor logic for auth requests & decrypt all fingerprints (#800)

This commit is contained in:
Caleb Derosier 2024-01-26 14:17:21 -07:00 committed by Álison Fernandes
parent a4e99745bc
commit a7e393e325
16 changed files with 367 additions and 194 deletions

View file

@ -16,7 +16,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
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.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
@ -184,11 +183,6 @@ interface AuthRepository : AuthenticatorProvider {
*/ */
suspend fun getAuthRequests(): AuthRequestsResult suspend fun getAuthRequests(): AuthRequestsResult
/**
* Gets a unique fingerprint phrase for this user.
*/
suspend fun getFingerprintPhrase(email: String): UserFingerprintResult
/** /**
* Get a [Boolean] indicating whether this is a known device. * Get a [Boolean] indicating whether this is a known device.
*/ */

View file

@ -582,42 +582,17 @@ class AuthRepositoryImpl(
authSdkSource authSdkSource
.getNewAuthRequest(email) .getNewAuthRequest(email)
.flatMap { authRequest -> .flatMap { authRequest ->
newAuthRequestService.createAuthRequest( newAuthRequestService
email = email, .createAuthRequest(
publicKey = authRequest.publicKey, email = email,
deviceId = authDiskSource.uniqueAppId, publicKey = authRequest.publicKey,
accessCode = authRequest.accessCode, deviceId = authDiskSource.uniqueAppId,
fingerprint = authRequest.fingerprint, accessCode = authRequest.accessCode,
) fingerprint = authRequest.fingerprint,
}
.fold(
onFailure = { AuthRequestResult.Error },
onSuccess = { request ->
AuthRequestResult.Success(
authRequest = AuthRequest(
id = request.id,
publicKey = request.publicKey,
platform = request.platform,
ipAddress = request.ipAddress,
key = request.key,
masterPasswordHash = request.masterPasswordHash,
creationDate = request.creationDate,
responseDate = request.responseDate,
requestApproved = request.requestApproved ?: false,
originUrl = request.originUrl,
),
) )
}, .map { request ->
) AuthRequestResult.Success(
authRequest = AuthRequest(
override suspend fun getAuthRequests(): AuthRequestsResult =
authRequestsService.getAuthRequests()
.fold(
onFailure = { AuthRequestsResult.Error },
onSuccess = { response ->
AuthRequestsResult.Success(
authRequests = response.authRequests.map { request ->
AuthRequest(
id = request.id, id = request.id,
publicKey = request.publicKey, publicKey = request.publicKey,
platform = request.platform, platform = request.platform,
@ -628,29 +603,45 @@ class AuthRepositoryImpl(
responseDate = request.responseDate, responseDate = request.responseDate,
requestApproved = request.requestApproved ?: false, requestApproved = request.requestApproved ?: false,
originUrl = request.originUrl, originUrl = request.originUrl,
) fingerprint = authRequest.fingerprint,
),
)
}
}
.fold(
onFailure = { AuthRequestResult.Error },
onSuccess = { it },
)
override suspend fun getAuthRequests(): AuthRequestsResult =
authRequestsService
.getAuthRequests()
.fold(
onFailure = { AuthRequestsResult.Error },
onSuccess = { response ->
AuthRequestsResult.Success(
authRequests = response.authRequests.mapNotNull { request ->
when (val result = getFingerprintPhrase(request.publicKey)) {
is UserFingerprintResult.Error -> null
is UserFingerprintResult.Success -> AuthRequest(
id = request.id,
publicKey = request.publicKey,
platform = request.platform,
ipAddress = request.ipAddress,
key = request.key,
masterPasswordHash = request.masterPasswordHash,
creationDate = request.creationDate,
responseDate = request.responseDate,
requestApproved = request.requestApproved ?: false,
originUrl = request.originUrl,
fingerprint = result.fingerprint,
)
}
}, },
) )
}, },
) )
override suspend fun getFingerprintPhrase(
email: String,
): UserFingerprintResult =
authSdkSource
.getNewAuthRequest(email)
.flatMap { requestResponse ->
authSdkSource
.getUserFingerprint(
email = email,
publicKey = requestResponse.publicKey,
)
}
.fold(
onFailure = { UserFingerprintResult.Error },
onSuccess = { UserFingerprintResult.Success(it) },
)
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult = override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
devicesService devicesService
.getIsKnownDevice( .getIsKnownDevice(
@ -690,6 +681,23 @@ class AuthRepositoryImpl(
}, },
) )
private suspend fun getFingerprintPhrase(
publicKey: String,
): UserFingerprintResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return UserFingerprintResult.Error
return authSdkSource
.getUserFingerprint(
email = profile.email,
publicKey = publicKey,
)
.fold(
onFailure = { UserFingerprintResult.Error },
onSuccess = { UserFingerprintResult.Success(it) },
)
}
/** /**
* Get the remembered two-factor token associated with the user's email, if applicable. * Get the remembered two-factor token associated with the user's email, if applicable.
*/ */

View file

@ -15,6 +15,7 @@ import java.time.ZonedDateTime
* @param responseDate The date & time on which this request was responded to. * @param responseDate The date & time on which this request was responded to.
* @param requestApproved Whether this request was approved. * @param requestApproved Whether this request was approved.
* @param originUrl The origin URL of this auth request. * @param originUrl The origin URL of this auth request.
* @param fingerprint The fingerprint of this auth request.
*/ */
data class AuthRequest( data class AuthRequest(
val id: String, val id: String,
@ -27,4 +28,5 @@ data class AuthRequest(
val responseDate: ZonedDateTime?, val responseDate: ZonedDateTime?,
val requestApproved: Boolean, val requestApproved: Boolean,
val originUrl: String, val originUrl: String,
val fingerprint: String,
) )

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.repository package com.x8bit.bitwarden.data.platform.repository
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@ -113,6 +114,11 @@ interface SettingsRepository {
*/ */
fun disableAutofill() fun disableAutofill()
/**
* Gets the unique fingerprint phrase for the current user.
*/
suspend fun getUserFingerprint(): UserFingerprintResult
/** /**
* Sets default values for various settings for the given [userId] if necessary. This is * Sets default values for various settings for the given [userId] if necessary. This is
* typically used when logging into a new account. * typically used when logging into a new account.

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import com.x8bit.bitwarden.BuildConfig import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -252,6 +253,19 @@ class SettingsRepositoryImpl(
mutableIsAutofillEnabledStateFlow.value = false mutableIsAutofillEnabledStateFlow.value = false
} }
@Suppress("ReturnCount")
override suspend fun getUserFingerprint(): UserFingerprintResult {
val userId = activeUserId
?: return UserFingerprintResult.Error
return vaultSdkSource
.getUserFingerprint(userId)
.fold(
onFailure = { UserFingerprintResult.Error },
onSuccess = { UserFingerprintResult.Success(it) },
)
}
override fun setDefaultsIfNecessary(userId: String) { override fun setDefaultsIfNecessary(userId: String) {
// Set Vault Settings defaults // Set Vault Settings defaults
if (!isVaultTimeoutActionSet(userId = userId)) { if (!isVaultTimeoutActionSet(userId = userId)) {

View file

@ -68,6 +68,11 @@ interface VaultSdkSource {
*/ */
suspend fun getUserEncryptionKey(userId: String): Result<String> suspend fun getUserEncryptionKey(userId: String): Result<String>
/**
* Gets the user's fingerprint.
*/
suspend fun getUserFingerprint(userId: String): Result<String>
/** /**
* Attempts to initialize cryptography functionality for an individual user with the given * Attempts to initialize cryptography functionality for an individual user with the given
* [userId] for the Bitwarden SDK with a given [InitUserCryptoRequest]. * [userId] for the Bitwarden SDK with a given [InitUserCryptoRequest].

View file

@ -65,6 +65,15 @@ class VaultSdkSourceImpl(
.getUserEncryptionKey() .getUserEncryptionKey()
} }
override suspend fun getUserFingerprint(
userId: String,
): Result<String> =
runCatching {
getClient(userId = userId)
.platform()
.userFingerprint(userId)
}
override suspend fun initializeCrypto( override suspend fun initializeCrypto(
userId: String, userId: String,
request: InitUserCryptoRequest, request: InitUserCryptoRequest,

View file

@ -6,7 +6,6 @@ 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.AuthRequestResult import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -34,14 +33,6 @@ class LoginWithDeviceViewModel @Inject constructor(
) { ) {
init { init {
sendNewAuthRequest() sendNewAuthRequest()
viewModelScope.launch {
trySendAction(
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
result = authRepository.getFingerprintPhrase(state.emailAddress),
),
)
}
} }
override fun handleAction(action: LoginWithDeviceAction) { override fun handleAction(action: LoginWithDeviceAction) {
@ -53,10 +44,6 @@ class LoginWithDeviceViewModel @Inject constructor(
is LoginWithDeviceAction.Internal.NewAuthRequestResultReceive -> { is LoginWithDeviceAction.Internal.NewAuthRequestResultReceive -> {
handleNewAuthRequestResultReceived(action) handleNewAuthRequestResultReceived(action)
} }
is LoginWithDeviceAction.Internal.FingerprintPhraseReceived -> {
handleFingerprintPhraseReceived(action)
}
} }
} }
@ -75,27 +62,20 @@ class LoginWithDeviceViewModel @Inject constructor(
private fun handleNewAuthRequestResultReceived( private fun handleNewAuthRequestResultReceived(
action: LoginWithDeviceAction.Internal.NewAuthRequestResultReceive, action: LoginWithDeviceAction.Internal.NewAuthRequestResultReceive,
) {
if (action.result is AuthRequestResult.Error) {
// TODO BIT-1563 handle error
}
}
private fun handleFingerprintPhraseReceived(
action: LoginWithDeviceAction.Internal.FingerprintPhraseReceived,
) { ) {
when (action.result) { when (action.result) {
is UserFingerprintResult.Success -> { is AuthRequestResult.Success -> {
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
viewState = LoginWithDeviceState.ViewState.Content( viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = action.result.fingerprint, fingerprintPhrase = action.result.authRequest.fingerprint,
), ),
) )
} }
} }
is UserFingerprintResult.Error -> { is AuthRequestResult.Error -> {
// TODO BIT-1563 display error dialog
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
viewState = LoginWithDeviceState.ViewState.Error( viewState = LoginWithDeviceState.ViewState.Error(
@ -209,12 +189,5 @@ sealed class LoginWithDeviceAction {
data class NewAuthRequestResultReceive( data class NewAuthRequestResultReceive(
val result: AuthRequestResult, val result: AuthRequestResult,
) : Internal() ) : Internal()
/**
* A fingerprint phrase for this user has been received.
*/
data class FingerprintPhraseReceived(
val result: UserFingerprintResult,
) : Internal()
} }
} }

View file

@ -5,6 +5,7 @@ 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.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
@ -18,6 +19,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import javax.inject.Inject import javax.inject.Inject
@ -38,7 +40,7 @@ class AccountSecurityViewModel @Inject constructor(
initialState = savedStateHandle[KEY_STATE] initialState = savedStateHandle[KEY_STATE]
?: AccountSecurityState( ?: AccountSecurityState(
dialog = null, dialog = null,
fingerprintPhrase = "fingerprint-placeholder".asText(), fingerprintPhrase = "".asText(), // This will be filled in dynamically
isApproveLoginRequestsEnabled = settingsRepository.isApprovePasswordlessLoginsEnabled, isApproveLoginRequestsEnabled = settingsRepository.isApprovePasswordlessLoginsEnabled,
isUnlockWithBiometricsEnabled = false, isUnlockWithBiometricsEnabled = false,
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled, isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
@ -59,6 +61,14 @@ class AccountSecurityViewModel @Inject constructor(
stateFlow stateFlow
.onEach { savedStateHandle[KEY_STATE] = it } .onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope) .launchIn(viewModelScope)
viewModelScope.launch {
trySendAction(
AccountSecurityAction.Internal.FingerprintResultReceive(
fingerprintResult = settingsRepository.getUserFingerprint(),
),
)
}
} }
override fun handleAction(action: AccountSecurityAction): Unit = when (action) { override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
@ -92,6 +102,10 @@ class AccountSecurityViewModel @Inject constructor(
is AccountSecurityAction.PushNotificationConfirm -> { is AccountSecurityAction.PushNotificationConfirm -> {
handlePushNotificationConfirm() handlePushNotificationConfirm()
} }
is AccountSecurityAction.Internal.FingerprintResultReceive -> {
handleFingerprintResultReceived(action)
}
} }
private fun handleAccountFingerprintPhraseClick() { private fun handleAccountFingerprintPhraseClick() {
@ -239,6 +253,20 @@ class AccountSecurityViewModel @Inject constructor(
} }
} }
} }
private fun handleFingerprintResultReceived(
action: AccountSecurityAction.Internal.FingerprintResultReceive,
) {
mutableStateFlow.update {
it.copy(
fingerprintPhrase = when (val result = action.fingerprintResult) {
is UserFingerprintResult.Success -> result.fingerprint.asText()
// This should never fail for an unlocked account.
is UserFingerprintResult.Error -> "".asText()
},
)
}
}
} }
/** /**
@ -474,4 +502,16 @@ sealed class AccountSecurityAction {
override val isUnlockWithPinEnabled: Boolean get() = true override val isUnlockWithPinEnabled: Boolean get() = true
} }
} }
/**
* Models actions that can be sent by the view model itself.
*/
sealed class Internal : AccountSecurityAction() {
/**
* A fingerprint has been received.
*/
data class FingerprintResultReceive(
val fingerprintResult: UserFingerprintResult,
) : Internal()
}
} }

View file

@ -77,7 +77,7 @@ class PendingRequestsViewModel @Inject constructor(
PendingRequestsState.ViewState.Content( PendingRequestsState.ViewState.Content(
requests = result.authRequests.map { authRequest -> requests = result.authRequests.map { authRequest ->
PendingRequestsState.ViewState.Content.PendingLoginRequest( PendingRequestsState.ViewState.Content.PendingLoginRequest(
fingerprintPhrase = authRequest.publicKey, fingerprintPhrase = authRequest.fingerprint,
platform = authRequest.platform, platform = authRequest.platform,
timestamp = dateTimeFormatter.format( timestamp = dateTimeFormatter.format(
authRequest.creationDate, authRequest.creationDate,

View file

@ -50,7 +50,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
@ -2135,6 +2134,7 @@ class AuthRepositoryTest {
responseDate = null, responseDate = null,
requestApproved = true, requestApproved = true,
originUrl = "www.bitwarden.com", originUrl = "www.bitwarden.com",
fingerprint = fingerprint,
), ),
) )
coEvery { coEvery {
@ -2179,11 +2179,12 @@ class AuthRepositoryTest {
@Test @Test
fun `getAuthRequests should return success when service returns success`() = runTest { fun `getAuthRequests should return success when service returns success`() = runTest {
val fingerprint = "fingerprint"
val responseJson = AuthRequestsResponseJson( val responseJson = AuthRequestsResponseJson(
authRequests = listOf( authRequests = listOf(
AuthRequestsResponseJson.AuthRequest( AuthRequestsResponseJson.AuthRequest(
id = "1", id = "1",
publicKey = "2", publicKey = PUBLIC_KEY,
platform = "Android", platform = "Android",
ipAddress = "192.168.0.1", ipAddress = "192.168.0.1",
key = "public", key = "public",
@ -2199,7 +2200,46 @@ class AuthRepositoryTest {
authRequests = listOf( authRequests = listOf(
AuthRequest( AuthRequest(
id = "1", id = "1",
publicKey = "2", publicKey = PUBLIC_KEY,
platform = "Android",
ipAddress = "192.168.0.1",
key = "public",
masterPasswordHash = "verySecureHash",
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
responseDate = null,
requestApproved = true,
originUrl = "www.bitwarden.com",
fingerprint = fingerprint,
),
),
)
coEvery {
authSdkSource.getUserFingerprint(
email = EMAIL,
publicKey = PUBLIC_KEY,
)
} returns Result.success(fingerprint)
coEvery {
authRequestsService.getAuthRequests()
} returns responseJson.asSuccess()
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val result = repository.getAuthRequests()
coVerify(exactly = 1) {
authRequestsService.getAuthRequests()
authSdkSource.getUserFingerprint(EMAIL, PUBLIC_KEY)
}
assertEquals(expected, result)
}
@Test
fun `getAuthRequests should return empty list when user profile is null`() = runTest {
val responseJson = AuthRequestsResponseJson(
authRequests = listOf(
AuthRequestsResponseJson.AuthRequest(
id = "1",
publicKey = PUBLIC_KEY,
platform = "Android", platform = "Android",
ipAddress = "192.168.0.1", ipAddress = "192.168.0.1",
key = "public", key = "public",
@ -2211,6 +2251,7 @@ class AuthRepositoryTest {
), ),
), ),
) )
val expected = AuthRequestsResult.Success(emptyList())
coEvery { coEvery {
authRequestsService.getAuthRequests() authRequestsService.getAuthRequests()
} returns responseJson.asSuccess() } returns responseJson.asSuccess()
@ -2223,68 +2264,6 @@ class AuthRepositoryTest {
assertEquals(expected, result) assertEquals(expected, result)
} }
@Test
fun `getUserFingerprint should return failure when source returns failure`() = runTest {
coEvery {
authSdkSource.getNewAuthRequest(EMAIL)
} returns Result.success(
mockk<AuthRequestResponse> {
every { publicKey } returns PUBLIC_KEY
},
)
coEvery {
authSdkSource.getUserFingerprint(
email = EMAIL,
publicKey = PUBLIC_KEY,
)
} returns Result.failure(Throwable())
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val result = repository.getFingerprintPhrase(EMAIL)
coVerify(exactly = 1) {
authSdkSource.getNewAuthRequest(EMAIL)
authSdkSource.getUserFingerprint(
email = EMAIL,
publicKey = PUBLIC_KEY,
)
}
assertEquals(UserFingerprintResult.Error, result)
}
@Test
fun `getUserFingerprint should return success when source returns success`() = runTest {
val fingerprint = "fingerprint"
coEvery {
authSdkSource.getNewAuthRequest(EMAIL)
} returns Result.success(
AuthRequestResponse(
fingerprint = fingerprint,
publicKey = PUBLIC_KEY,
privateKey = "key",
accessCode = "accessCode",
),
)
coEvery {
authSdkSource.getUserFingerprint(
email = EMAIL,
publicKey = PUBLIC_KEY,
)
} returns Result.success(fingerprint)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val result = repository.getFingerprintPhrase(EMAIL)
coVerify(exactly = 1) {
authSdkSource.getNewAuthRequest(EMAIL)
authSdkSource.getUserFingerprint(
email = EMAIL,
publicKey = PUBLIC_KEY,
)
}
assertEquals(UserFingerprintResult.Success(fingerprint), result)
}
@Test @Test
fun `getIsKnownDevice should return failure when service returns failure`() = runTest { fun `getIsKnownDevice should return failure when service returns failure`() = runTest {
coEvery { coEvery {
@ -2462,7 +2441,7 @@ class AuthRepositoryTest {
private val ACCOUNT_1 = AccountJson( private val ACCOUNT_1 = AccountJson(
profile = AccountJson.Profile( profile = AccountJson.Profile(
userId = USER_ID_1, userId = USER_ID_1,
email = "test@bitwarden.com", email = EMAIL,
isEmailVerified = true, isEmailVerified = true,
name = "Bitwarden Tester", name = "Bitwarden Tester",
hasPremium = false, hasPremium = false,

View file

@ -5,6 +5,7 @@ import app.cash.turbine.test
import com.bitwarden.core.DerivePinKeyResponse import com.bitwarden.core.DerivePinKeyResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
@ -555,6 +556,56 @@ class SettingsRepositoryTest {
verify { autofillManager.disableAutofillServices() } verify { autofillManager.disableAutofillServices() }
} }
@Test
fun `getUserFingerprint should return failure with no active user`() = runTest {
fakeAuthDiskSource.userState = null
val result = settingsRepository.getUserFingerprint()
assertEquals(UserFingerprintResult.Error, result)
}
@Test
fun `getUserFingerprint should return failure with active user when source returns failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultSdkSource.getUserFingerprint(
userId = MOCK_USER_STATE.activeUserId,
)
} returns Result.failure(Throwable())
val result = settingsRepository.getUserFingerprint()
coVerify(exactly = 1) {
vaultSdkSource.getUserFingerprint(
userId = MOCK_USER_STATE.activeUserId,
)
}
assertEquals(UserFingerprintResult.Error, result)
}
@Test
fun `getUserFingerprint should return success with active user when source returns success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val fingerprint = "fingerprint"
coEvery {
vaultSdkSource.getUserFingerprint(
userId = MOCK_USER_STATE.activeUserId,
)
} returns Result.success(fingerprint)
val result = settingsRepository.getUserFingerprint()
coVerify(exactly = 1) {
vaultSdkSource.getUserFingerprint(
userId = MOCK_USER_STATE.activeUserId,
)
}
assertEquals(UserFingerprintResult.Success(fingerprint), result)
}
@Test @Test
fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest { fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest {
val userId = "userId" val userId = "userId"
@ -728,26 +779,27 @@ class SettingsRepositoryTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `isScreenCaptureAllowed property should update SettingsDiskSource and emit changes`() = runTest { fun `isScreenCaptureAllowed property should update SettingsDiskSource and emit changes`() =
val userId = "userId" runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE val userId = "userId"
fakeAuthDiskSource.userState = MOCK_USER_STATE
fakeSettingsDiskSource.storeScreenCaptureAllowed(userId, false) fakeSettingsDiskSource.storeScreenCaptureAllowed(userId, false)
settingsRepository.isScreenCaptureAllowedStateFlow.test { settingsRepository.isScreenCaptureAllowedStateFlow.test {
assertFalse(awaitItem()) assertFalse(awaitItem())
settingsRepository.isScreenCaptureAllowed = true settingsRepository.isScreenCaptureAllowed = true
assertTrue(awaitItem()) assertTrue(awaitItem())
assertEquals(true, fakeSettingsDiskSource.getScreenCaptureAllowed(userId)) assertEquals(true, fakeSettingsDiskSource.getScreenCaptureAllowed(userId))
settingsRepository.isScreenCaptureAllowed = false settingsRepository.isScreenCaptureAllowed = false
assertFalse(awaitItem()) assertFalse(awaitItem())
assertEquals(false, fakeSettingsDiskSource.getScreenCaptureAllowed(userId)) assertEquals(false, fakeSettingsDiskSource.getScreenCaptureAllowed(userId))
}
} }
}
} }
private val MOCK_USER_STATE = private val MOCK_USER_STATE =

View file

@ -22,6 +22,7 @@ import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.Client import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientCrypto import com.bitwarden.sdk.ClientCrypto
import com.bitwarden.sdk.ClientPasswordHistory import com.bitwarden.sdk.ClientPasswordHistory
import com.bitwarden.sdk.ClientPlatform
import com.bitwarden.sdk.ClientVault import com.bitwarden.sdk.ClientVault
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asFailure
@ -42,12 +43,14 @@ import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class VaultSdkSourceTest { class VaultSdkSourceTest {
private val clientCrypto = mockk<ClientCrypto>() private val clientCrypto = mockk<ClientCrypto>()
private val clientPlatform = mockk<ClientPlatform>()
private val clientPasswordHistory = mockk<ClientPasswordHistory>() private val clientPasswordHistory = mockk<ClientPasswordHistory>()
private val clientVault = mockk<ClientVault>() { private val clientVault = mockk<ClientVault>() {
every { passwordHistory() } returns clientPasswordHistory every { passwordHistory() } returns clientPasswordHistory
} }
private val client = mockk<Client>() { private val client = mockk<Client>() {
every { vault() } returns clientVault every { vault() } returns clientVault
every { platform() } returns clientPlatform
every { crypto() } returns clientCrypto every { crypto() } returns clientCrypto
} }
private val sdkClientManager = mockk<SdkClientManager> { private val sdkClientManager = mockk<SdkClientManager> {
@ -132,6 +135,28 @@ class VaultSdkSourceTest {
verify { sdkClientManager.getOrCreateClient(userId = userId) } verify { sdkClientManager.getOrCreateClient(userId = userId) }
} }
@Test
fun `getUserFingerprint should call SDK and return a Result with correct data`() = runBlocking {
val userId = "userId"
val expectedResult = "fingerprint"
coEvery {
clientPlatform.userFingerprint(
fingerprintMaterial = userId,
)
} returns expectedResult
val result = vaultSdkSource.getUserFingerprint(userId)
assertEquals(
expectedResult.asSuccess(),
result,
)
coVerify {
clientPlatform.userFingerprint(
fingerprintMaterial = userId,
)
}
}
@Test @Test
fun `initializeUserCrypto with sdk success should return InitializeCryptoResult Success`() = fun `initializeUserCrypto with sdk success should return InitializeCryptoResult Success`() =
runBlocking { runBlocking {

View file

@ -4,26 +4,25 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
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.AuthRequest
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
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.Test import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
class LoginWithDeviceViewModelTest : BaseViewModelTest() { class LoginWithDeviceViewModelTest : BaseViewModelTest() {
private val authRepository = mockk<AuthRepository> { private val authRepository = mockk<AuthRepository> {
coEvery {
getFingerprintPhrase(EMAIL)
} returns UserFingerprintResult.Success("initialFingerprint")
coEvery { coEvery {
createAuthRequest(EMAIL) createAuthRequest(EMAIL)
} returns mockk<AuthRequestResult.Success>() } returns AuthRequestResult.Success(AUTH_REQUEST)
} }
@Test @Test
@ -33,7 +32,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
assertEquals(DEFAULT_STATE, awaitItem()) assertEquals(DEFAULT_STATE, awaitItem())
} }
coVerify { authRepository.createAuthRequest(EMAIL) } coVerify { authRepository.createAuthRequest(EMAIL) }
coVerify { authRepository.getFingerprintPhrase(EMAIL) }
} }
@Test @Test
@ -42,14 +40,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
coEvery { coEvery {
authRepository.createAuthRequest(newEmail) authRepository.createAuthRequest(newEmail)
} returns mockk<AuthRequestResult.Success>() } returns AuthRequestResult.Success(AUTH_REQUEST)
coEvery {
authRepository.getFingerprintPhrase(newEmail)
} returns UserFingerprintResult.Success("initialFingerprint")
val state = LoginWithDeviceState( val state = LoginWithDeviceState(
emailAddress = newEmail, emailAddress = newEmail,
viewState = LoginWithDeviceState.ViewState.Content( viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "initialFingerprint", fingerprintPhrase = FINGERPRINT,
), ),
) )
val viewModel = createViewModel(state) val viewModel = createViewModel(state)
@ -58,7 +53,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
} }
coVerify { coVerify {
authRepository.createAuthRequest(newEmail) authRepository.createAuthRequest(newEmail)
authRepository.getFingerprintPhrase(newEmail)
} }
} }
@ -101,13 +95,17 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `on fingerprint result success received should show content`() = runTest { fun `on auth request result success received should show content`() = runTest {
val newFingerprint = "newFingerprint" val newFingerprint = "newFingerprint"
val viewModel = createViewModel() val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
viewModel.actionChannel.trySend( viewModel.actionChannel.trySend(
LoginWithDeviceAction.Internal.FingerprintPhraseReceived( LoginWithDeviceAction.Internal.NewAuthRequestResultReceive(
result = UserFingerprintResult.Success(newFingerprint), result = AuthRequestResult.Success(
authRequest = mockk<AuthRequest> {
every { fingerprint } returns newFingerprint
},
),
), ),
) )
assertEquals( assertEquals(
@ -125,8 +123,8 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
viewModel.actionChannel.trySend( viewModel.actionChannel.trySend(
LoginWithDeviceAction.Internal.FingerprintPhraseReceived( LoginWithDeviceAction.Internal.NewAuthRequestResultReceive(
result = UserFingerprintResult.Error, result = AuthRequestResult.Error,
), ),
) )
assertEquals( assertEquals(
@ -149,11 +147,25 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
companion object { companion object {
private const val EMAIL = "test@gmail.com" private const val EMAIL = "test@gmail.com"
private const val FINGERPRINT = "fingerprint"
private val DEFAULT_STATE = LoginWithDeviceState( private val DEFAULT_STATE = LoginWithDeviceState(
emailAddress = EMAIL, emailAddress = EMAIL,
viewState = LoginWithDeviceState.ViewState.Content( viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "initialFingerprint", fingerprintPhrase = FINGERPRINT,
), ),
) )
private val AUTH_REQUEST = AuthRequest(
id = "1",
publicKey = "2",
platform = "Android",
ipAddress = "192.168.0.1",
key = "public",
masterPasswordHash = "verySecureHash",
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
responseDate = null,
requestApproved = true,
originUrl = "www.bitwarden.com",
fingerprint = FINGERPRINT,
)
} }
} }

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
@ -12,6 +13,8 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
@ -29,8 +32,13 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
@Test @Test
fun `initial state should be correct when saved state is set`() { fun `initial state should be correct when saved state is set`() {
val viewModel = createViewModel(initialState = DEFAULT_STATE) val settingsRepository = getMockSettingsRepository()
val viewModel = createViewModel(
initialState = DEFAULT_STATE,
settingsRepository = settingsRepository,
)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
coVerify { settingsRepository.getUserFingerprint() }
} }
@Test @Test
@ -40,6 +48,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
every { isApprovePasswordlessLoginsEnabled } returns false every { isApprovePasswordlessLoginsEnabled } returns false
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel( val viewModel = createViewModel(
initialState = null, initialState = null,
@ -49,6 +58,33 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true), DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
coVerify { settingsRepository.getUserFingerprint() }
}
@Test
fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest {
val fingerprint = "fingerprint"
val viewModel = createViewModel()
// Set fingerprint phrase to value received
viewModel.trySendAction(
AccountSecurityAction.Internal.FingerprintResultReceive(
UserFingerprintResult.Success(fingerprint),
),
)
assertEquals(
DEFAULT_STATE.copy(fingerprintPhrase = fingerprint.asText()),
viewModel.stateFlow.value,
)
// Clear fingerprint phrase
viewModel.trySendAction(
AccountSecurityAction.Internal.FingerprintResultReceive(
UserFingerprintResult.Error,
),
)
assertEquals(
DEFAULT_STATE.copy(fingerprintPhrase = "".asText()),
viewModel.stateFlow.value,
)
} }
@Test @Test
@ -151,6 +187,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
fun `on VaultTimeoutTypeSelect should update the selection()`() = runTest { fun `on VaultTimeoutTypeSelect should update the selection()`() = runTest {
val settingsRepository = mockk<SettingsRepository>() { val settingsRepository = mockk<SettingsRepository>() {
every { vaultTimeout = any() } just runs every { vaultTimeout = any() } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.trySendAction( viewModel.trySendAction(
@ -169,6 +206,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
fun `on CustomVaultTimeoutSelect should update the selection()`() = runTest { fun `on CustomVaultTimeoutSelect should update the selection()`() = runTest {
val settingsRepository = mockk<SettingsRepository>() { val settingsRepository = mockk<SettingsRepository>() {
every { vaultTimeout = any() } just runs every { vaultTimeout = any() } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.trySendAction( viewModel.trySendAction(
@ -191,6 +229,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
fun `on VaultTimeoutActionSelect should update vault timeout action`() = runTest { fun `on VaultTimeoutActionSelect should update vault timeout action`() = runTest {
val settingsRepository = mockk<SettingsRepository>() { val settingsRepository = mockk<SettingsRepository>() {
every { vaultTimeoutAction = any() } just runs every { vaultTimeoutAction = any() } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel(settingsRepository = settingsRepository) val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.trySendAction( viewModel.trySendAction(
@ -257,6 +296,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
) )
val settingsRepository: SettingsRepository = mockk() { val settingsRepository: SettingsRepository = mockk() {
every { clearUnlockPin() } just runs every { clearUnlockPin() } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel( val viewModel = createViewModel(
initialState = initialState, initialState = initialState,
@ -296,6 +336,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
) )
val settingsRepository: SettingsRepository = mockk() { val settingsRepository: SettingsRepository = mockk() {
every { storeUnlockPin(any(), any()) } just runs every { storeUnlockPin(any(), any()) } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel( val viewModel = createViewModel(
initialState = initialState, initialState = initialState,
@ -353,6 +394,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
runTest { runTest {
val settingsRepository = mockk<SettingsRepository> { val settingsRepository = mockk<SettingsRepository> {
every { isApprovePasswordlessLoginsEnabled = true } just runs every { isApprovePasswordlessLoginsEnabled = true } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel( val viewModel = createViewModel(
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
@ -387,6 +429,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
runTest { runTest {
val settingsRepository = mockk<SettingsRepository> { val settingsRepository = mockk<SettingsRepository> {
every { isApprovePasswordlessLoginsEnabled = false } just runs every { isApprovePasswordlessLoginsEnabled = false } just runs
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
} }
val viewModel = createViewModel( val viewModel = createViewModel(
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
@ -416,13 +459,21 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
} }
} }
/**
* Returns a [mockk] of the [SettingsRepository] with the call made on init already mocked.
*/
private fun getMockSettingsRepository(): SettingsRepository =
mockk<SettingsRepository> {
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
}
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun createViewModel( private fun createViewModel(
initialState: AccountSecurityState? = DEFAULT_STATE, initialState: AccountSecurityState? = DEFAULT_STATE,
authRepository: AuthRepository = mockk(relaxed = true), authRepository: AuthRepository = mockk(relaxed = true),
vaultRepository: VaultRepository = mockk(relaxed = true), vaultRepository: VaultRepository = mockk(relaxed = true),
settingsRepository: SettingsRepository = mockk(relaxed = true),
environmentRepository: EnvironmentRepository = fakeEnvironmentRepository, environmentRepository: EnvironmentRepository = fakeEnvironmentRepository,
settingsRepository: SettingsRepository = getMockSettingsRepository(),
savedStateHandle: SavedStateHandle = SavedStateHandle().apply { savedStateHandle: SavedStateHandle = SavedStateHandle().apply {
set("state", initialState) set("state", initialState)
}, },
@ -435,9 +486,10 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
) )
companion object { companion object {
private const val FINGERPRINT = "fingerprint"
private val DEFAULT_STATE = AccountSecurityState( private val DEFAULT_STATE = AccountSecurityState(
dialog = null, dialog = null,
fingerprintPhrase = "fingerprint-placeholder".asText(), fingerprintPhrase = FINGERPRINT.asText(),
isApproveLoginRequestsEnabled = false, isApproveLoginRequestsEnabled = false,
isUnlockWithBiometricsEnabled = false, isUnlockWithBiometricsEnabled = false,
isUnlockWithPinEnabled = false, isUnlockWithPinEnabled = false,

View file

@ -56,7 +56,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
authRequests = listOf( authRequests = listOf(
AuthRequest( AuthRequest(
id = "1", id = "1",
publicKey = "pantry-overdue-survive-sleep-jab", publicKey = "publicKey-1",
platform = "Android", platform = "Android",
ipAddress = "192.168.0.1", ipAddress = "192.168.0.1",
key = "publicKey", key = "publicKey",
@ -65,10 +65,11 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
responseDate = null, responseDate = null,
requestApproved = true, requestApproved = true,
originUrl = "www.bitwarden.com", originUrl = "www.bitwarden.com",
fingerprint = "pantry-overdue-survive-sleep-jab",
), ),
AuthRequest( AuthRequest(
id = "2", id = "2",
publicKey = "erupt-anew-matchbook-disk-student", publicKey = "publicKey-2",
platform = "iOS", platform = "iOS",
ipAddress = "192.168.0.2", ipAddress = "192.168.0.2",
key = "publicKey", key = "publicKey",
@ -77,6 +78,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
responseDate = null, responseDate = null,
requestApproved = false, requestApproved = false,
originUrl = "www.bitwarden.com", originUrl = "www.bitwarden.com",
fingerprint = "erupt-anew-matchbook-disk-student",
), ),
), ),
) )