mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Refactor logic for auth requests & decrypt all fingerprints (#800)
This commit is contained in:
parent
a4e99745bc
commit
a7e393e325
16 changed files with 367 additions and 194 deletions
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue