mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1565: Approve and decline login requests (#818)
This commit is contained in:
parent
3be37766e2
commit
a187fbb0d1
19 changed files with 726 additions and 71 deletions
|
@ -1,10 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /auth-requests API.
|
||||
|
@ -19,6 +22,15 @@ interface AuthRequestsApi {
|
|||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Updates an authentication request.
|
||||
*/
|
||||
@PUT("/auth-requests/{id}")
|
||||
suspend fun updateAuthRequest(
|
||||
@Path("id") userId: String,
|
||||
@Body body: AuthRequestUpdateRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Gets a list of auth requests for this device.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for updating an auth request.
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthRequestUpdateRequestJson(
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String?,
|
||||
|
||||
@SerialName("deviceIdentifier")
|
||||
val deviceId: String,
|
||||
|
||||
@SerialName("requestApproved")
|
||||
val isApproved: Boolean,
|
||||
)
|
|
@ -10,4 +10,15 @@ interface AuthRequestsService {
|
|||
* Gets the list of auth requests for the current user.
|
||||
*/
|
||||
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
|
||||
|
||||
/**
|
||||
* Updates an approval request.
|
||||
*/
|
||||
suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
key: String,
|
||||
masterPasswordHash: String?,
|
||||
deviceId: String,
|
||||
isApproved: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
|
||||
class AuthRequestsServiceImpl(
|
||||
|
@ -8,4 +9,21 @@ class AuthRequestsServiceImpl(
|
|||
) : AuthRequestsService {
|
||||
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
|
||||
authRequestsApi.getAuthRequests()
|
||||
|
||||
override suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
key: String,
|
||||
masterPasswordHash: String?,
|
||||
deviceId: String,
|
||||
isApproved: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
authRequestsApi.updateAuthRequest(
|
||||
userId = requestId,
|
||||
body = AuthRequestUpdateRequestJson(
|
||||
key = key,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
deviceId = deviceId,
|
||||
isApproved = isApproved,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -196,6 +196,17 @@ interface AuthRepository : AuthenticatorProvider {
|
|||
*/
|
||||
suspend fun getAuthRequests(): AuthRequestsResult
|
||||
|
||||
/**
|
||||
* Approves or declines the request corresponding to this [requestId] based on [publicKey]
|
||||
* according to [isApproved].
|
||||
*/
|
||||
suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
masterPasswordHash: String?,
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult
|
||||
|
||||
/**
|
||||
* Get a [Boolean] indicating whether this is a known device.
|
||||
*/
|
||||
|
|
|
@ -59,6 +59,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
|||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -76,7 +77,7 @@ import javax.inject.Singleton
|
|||
/**
|
||||
* Default implementation of [AuthRepository].
|
||||
*/
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
@Singleton
|
||||
class AuthRepositoryImpl(
|
||||
private val accountsService: AccountsService,
|
||||
|
@ -87,6 +88,7 @@ class AuthRepositoryImpl(
|
|||
private val newAuthRequestService: NewAuthRequestService,
|
||||
private val organizationService: OrganizationService,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
|
@ -693,6 +695,51 @@ class AuthRepositoryImpl(
|
|||
},
|
||||
)
|
||||
|
||||
override suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
masterPasswordHash: String?,
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult {
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error
|
||||
return vaultSdkSource
|
||||
.getAuthRequestKey(
|
||||
publicKey = publicKey,
|
||||
userId = userId,
|
||||
)
|
||||
.flatMap {
|
||||
authRequestsService
|
||||
.updateAuthRequest(
|
||||
requestId = requestId,
|
||||
key = it,
|
||||
deviceId = authDiskSource.uniqueAppId,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
isApproved = isApproved,
|
||||
)
|
||||
}
|
||||
.map { 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,
|
||||
fingerprint = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
|
||||
devicesService
|
||||
.getIsKnownDevice(
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
|||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -41,6 +42,7 @@ object AuthRepositoryModule {
|
|||
newAuthRequestService: NewAuthRequestService,
|
||||
organizationService: OrganizationService,
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
dispatchers: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
|
@ -55,6 +57,7 @@ object AuthRepositoryModule {
|
|||
newAuthRequestService = newAuthRequestService,
|
||||
organizationService = organizationService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatchers,
|
||||
|
|
|
@ -59,6 +59,14 @@ interface VaultSdkSource {
|
|||
encryptedPin: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Gets the key for an auth request that is required to approve or decline it.
|
||||
*/
|
||||
suspend fun getAuthRequestKey(
|
||||
publicKey: String,
|
||||
userId: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Gets the user's encryption key, which can be used to later unlock their vault via a call to
|
||||
* [initializeCrypto] with [InitUserCryptoMethod.DecryptedKey].
|
||||
|
|
|
@ -56,6 +56,16 @@ class VaultSdkSourceImpl(
|
|||
.derivePinUserKey(encryptedPin = encryptedPin)
|
||||
}
|
||||
|
||||
override suspend fun getAuthRequestKey(
|
||||
publicKey: String,
|
||||
userId: String,
|
||||
): Result<String> =
|
||||
runCatching {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.approveAuthRequest(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun getUserEncryptionKey(
|
||||
userId: String,
|
||||
): Result<String> =
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.ui.platform.base.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
/**
|
||||
* Creates a side effect to observe lifecycle events.
|
||||
*/
|
||||
@Composable
|
||||
fun LivecycleEventEffect(
|
||||
onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit,
|
||||
) {
|
||||
val eventHandler = rememberUpdatedState(onEvent)
|
||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
DisposableEffect(lifecycleOwner.value) {
|
||||
val lifecycle = lifecycleOwner.value.lifecycle
|
||||
val observer = LifecycleEventObserver { owner, event ->
|
||||
eventHandler.value(owner, event)
|
||||
}
|
||||
|
||||
lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,6 +29,9 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
|
@ -61,6 +64,20 @@ fun LoginApprovalScreen(
|
|||
}
|
||||
}
|
||||
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = if (state.shouldShowErrorDialog) {
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
)
|
||||
} else {
|
||||
BasicDialogState.Hidden
|
||||
},
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginApprovalAction.ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginap
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -29,6 +30,10 @@ class LoginApprovalViewModel @Inject constructor(
|
|||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: LoginApprovalState(
|
||||
fingerprint = LoginApprovalArgs(savedStateHandle).fingerprint,
|
||||
masterPasswordHash = null,
|
||||
publicKey = "",
|
||||
requestId = "",
|
||||
shouldShowErrorDialog = false,
|
||||
viewState = LoginApprovalState.ViewState.Loading,
|
||||
),
|
||||
) {
|
||||
|
@ -52,16 +57,35 @@ class LoginApprovalViewModel @Inject constructor(
|
|||
LoginApprovalAction.ApproveRequestClick -> handleApproveRequestClicked()
|
||||
LoginApprovalAction.CloseClick -> handleCloseClicked()
|
||||
LoginApprovalAction.DeclineRequestClick -> handleDeclineRequestClicked()
|
||||
LoginApprovalAction.ErrorDialogDismiss -> handleErrorDialogDismissed()
|
||||
|
||||
is LoginApprovalAction.Internal.ApproveRequestResultReceive -> {
|
||||
handleApproveRequestResultReceived(action)
|
||||
}
|
||||
|
||||
is LoginApprovalAction.Internal.AuthRequestResultReceive -> {
|
||||
handleAuthRequestResultReceived(action)
|
||||
}
|
||||
|
||||
is LoginApprovalAction.Internal.DeclineRequestResultReceive -> {
|
||||
handleDeclineRequestResultReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleApproveRequestClicked() {
|
||||
// TODO BIT-1565 implement approve login request
|
||||
sendEvent(LoginApprovalEvent.ShowToast("Not yet implemented".asText()))
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginApprovalAction.Internal.DeclineRequestResultReceive(
|
||||
result = authRepository.updateAuthRequest(
|
||||
requestId = mutableStateFlow.value.requestId,
|
||||
masterPasswordHash = mutableStateFlow.value.masterPasswordHash,
|
||||
publicKey = mutableStateFlow.value.publicKey,
|
||||
isApproved = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClicked() {
|
||||
|
@ -69,33 +93,87 @@ class LoginApprovalViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleDeclineRequestClicked() {
|
||||
// TODO BIT-1565 implement decline login request
|
||||
sendEvent(LoginApprovalEvent.ShowToast("Not yet implemented".asText()))
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginApprovalAction.Internal.DeclineRequestResultReceive(
|
||||
result = authRepository.updateAuthRequest(
|
||||
requestId = mutableStateFlow.value.requestId,
|
||||
masterPasswordHash = mutableStateFlow.value.masterPasswordHash,
|
||||
publicKey = mutableStateFlow.value.publicKey,
|
||||
isApproved = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorDialogDismissed() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowErrorDialog = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleApproveRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.ApproveRequestResultReceive,
|
||||
) {
|
||||
when (action.result) {
|
||||
is AuthRequestResult.Success -> {
|
||||
sendEvent(LoginApprovalEvent.ShowToast(R.string.login_approved.asText()))
|
||||
sendEvent(LoginApprovalEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is AuthRequestResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowErrorDialog = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.AuthRequestResultReceive,
|
||||
) {
|
||||
val email = authRepository.userStateFlow.value?.activeAccount?.email ?: return
|
||||
mutableStateFlow.update {
|
||||
when (val result = action.authRequestResult) {
|
||||
is AuthRequestResult.Success -> mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = when (val result = action.authRequestResult) {
|
||||
is AuthRequestResult.Success -> {
|
||||
LoginApprovalState.ViewState.Content(
|
||||
masterPasswordHash = result.authRequest.masterPasswordHash,
|
||||
publicKey = result.authRequest.publicKey,
|
||||
requestId = result.authRequest.id,
|
||||
viewState = LoginApprovalState.ViewState.Content(
|
||||
deviceType = result.authRequest.platform,
|
||||
domainUrl = result.authRequest.originUrl,
|
||||
email = email,
|
||||
fingerprint = result.authRequest.fingerprint,
|
||||
ipAddress = result.authRequest.ipAddress,
|
||||
time = dateTimeFormatter.format(result.authRequest.creationDate),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is AuthRequestResult.Error -> LoginApprovalState.ViewState.Error
|
||||
},
|
||||
is AuthRequestResult.Error -> mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginApprovalState.ViewState.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeclineRequestResultReceived(
|
||||
action: LoginApprovalAction.Internal.DeclineRequestResultReceive,
|
||||
) {
|
||||
when (action.result) {
|
||||
is AuthRequestResult.Success -> {
|
||||
sendEvent(LoginApprovalEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is AuthRequestResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowErrorDialog = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,8 +181,13 @@ class LoginApprovalViewModel @Inject constructor(
|
|||
*/
|
||||
@Parcelize
|
||||
data class LoginApprovalState(
|
||||
val fingerprint: String,
|
||||
val viewState: ViewState,
|
||||
val shouldShowErrorDialog: Boolean,
|
||||
// Internal
|
||||
val fingerprint: String,
|
||||
val masterPasswordHash: String?,
|
||||
val publicKey: String,
|
||||
val requestId: String,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the specific view states for the [LoginApprovalScreen].
|
||||
|
@ -176,15 +259,34 @@ sealed class LoginApprovalAction {
|
|||
*/
|
||||
data object DeclineRequestClick : LoginApprovalAction()
|
||||
|
||||
/**
|
||||
* User dismissed the error dialog.
|
||||
*/
|
||||
data object ErrorDialogDismiss : LoginApprovalAction()
|
||||
|
||||
/**
|
||||
* Models action the view model could send itself.
|
||||
*/
|
||||
sealed class Internal : LoginApprovalAction() {
|
||||
/**
|
||||
* A new result for a request to approve this request has been received.
|
||||
*/
|
||||
data class ApproveRequestResultReceive(
|
||||
val result: AuthRequestResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* An auth request result has been received to populate the data on the screen.
|
||||
*/
|
||||
data class AuthRequestResultReceive(
|
||||
val authRequestResult: AuthRequestResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* A new result for a request to decline this request has been received.
|
||||
*/
|
||||
data class DeclineRequestResultReceive(
|
||||
val result: AuthRequestResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,8 +35,10 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButtonWithIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
|
@ -72,6 +74,15 @@ fun PendingRequestsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LivecycleEventEffect { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
viewModel.trySendAction(PendingRequestsAction.LifecycleResume)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -23,7 +23,7 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class PendingRequestsViewModel @Inject constructor(
|
||||
authRepository: AuthRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<PendingRequestsState, PendingRequestsEvent, PendingRequestsAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: PendingRequestsState(
|
||||
|
@ -36,19 +36,14 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
.withZone(TimeZone.getDefault().toZoneId())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
PendingRequestsAction.Internal.AuthRequestsResultReceive(
|
||||
authRequestsResult = authRepository.getAuthRequests(),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateAuthRequestList()
|
||||
}
|
||||
|
||||
override fun handleAction(action: PendingRequestsAction) {
|
||||
when (action) {
|
||||
PendingRequestsAction.CloseClick -> handleCloseClicked()
|
||||
PendingRequestsAction.DeclineAllRequestsClick -> handleDeclineAllRequestsClicked()
|
||||
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
|
||||
is PendingRequestsAction.PendingRequestRowClick -> {
|
||||
handlePendingRequestRowClicked(action)
|
||||
}
|
||||
|
@ -67,6 +62,10 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
sendEvent(PendingRequestsEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleOnLifecycleResumed() {
|
||||
updateAuthRequestList()
|
||||
}
|
||||
|
||||
private fun handlePendingRequestRowClicked(
|
||||
action: PendingRequestsAction.PendingRequestRowClick,
|
||||
) {
|
||||
|
@ -102,6 +101,17 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAuthRequestList() {
|
||||
// TODO BIT-1574: Display pull to refresh
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
PendingRequestsAction.Internal.AuthRequestsResultReceive(
|
||||
authRequestsResult = authRepository.getAuthRequests(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -195,6 +205,11 @@ sealed class PendingRequestsAction {
|
|||
*/
|
||||
data object DeclineAllRequestsClick : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The screen has been re-opened and should be updated.
|
||||
*/
|
||||
data object LifecycleResume : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The user has clicked one of the pending request rows.
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.create
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class AuthRequestsServiceTest : BaseServiceTest() {
|
||||
|
||||
|
@ -45,4 +48,57 @@ class AuthRequestsServiceTest : BaseServiceTest() {
|
|||
val actual = service.getAuthRequests()
|
||||
assertTrue(actual.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateAuthRequest when request response is Failure should return Failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val actual = service.updateAuthRequest(
|
||||
requestId = "userId",
|
||||
deviceId = "deviceId",
|
||||
key = "secureKey",
|
||||
masterPasswordHash = null,
|
||||
isApproved = true,
|
||||
)
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateAuthRequest when request response is Success should return Success`() = runTest {
|
||||
val json = """
|
||||
{
|
||||
"id": "1",
|
||||
"publicKey": "2",
|
||||
"requestDeviceType": "Android",
|
||||
"requestIpAddress": "1.0.0.1",
|
||||
"key": "key",
|
||||
"masterPasswordHash": "verySecureHash",
|
||||
"creationDate": "2024-09-13T01:00:00.00Z",
|
||||
"requestApproved": true,
|
||||
"origin": "www.bitwarden.com"
|
||||
}
|
||||
"""
|
||||
val expected = AuthRequestsResponseJson.AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
platform = "Android",
|
||||
ipAddress = "1.0.0.1",
|
||||
key = "key",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
)
|
||||
val response = MockResponse().setBody(json).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val actual = service.updateAuthRequest(
|
||||
requestId = "userId",
|
||||
deviceId = "deviceId",
|
||||
key = "secureKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
isApproved = true,
|
||||
)
|
||||
assertEquals(Result.success(expected), actual)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito
|
|||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
|
@ -158,6 +159,14 @@ class AuthRepositoryTest {
|
|||
),
|
||||
)
|
||||
}
|
||||
private val vaultSdkSource = mockk<VaultSdkSource> {
|
||||
coEvery {
|
||||
getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
} returns "AsymmetricEncString".asSuccess()
|
||||
}
|
||||
private val userLogoutManager: UserLogoutManager = mockk {
|
||||
every { logout(any()) } just runs
|
||||
}
|
||||
|
@ -173,6 +182,7 @@ class AuthRepositoryTest {
|
|||
newAuthRequestService = newAuthRequestService,
|
||||
organizationService = organizationService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
|
@ -2413,6 +2423,143 @@ class AuthRepositoryTest {
|
|||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateAuthRequest should return failure when sdk returns failure`() = runTest {
|
||||
coEvery {
|
||||
vaultSdkSource.getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.updateAuthRequest(
|
||||
requestId = "requestId",
|
||||
masterPasswordHash = "masterPasswordHash",
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
}
|
||||
assertEquals(AuthRequestResult.Error, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateAuthRequest should return failure when service returns failure`() = runTest {
|
||||
val requestId = "requestId"
|
||||
val passwordHash = "masterPasswordHash"
|
||||
val encodedKey = "encodedKey"
|
||||
coEvery {
|
||||
vaultSdkSource.getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
} returns encodedKey.asSuccess()
|
||||
coEvery {
|
||||
authRequestsService.updateAuthRequest(
|
||||
requestId = requestId,
|
||||
masterPasswordHash = passwordHash,
|
||||
key = encodedKey,
|
||||
deviceId = UNIQUE_APP_ID,
|
||||
isApproved = false,
|
||||
)
|
||||
} returns Throwable("Mission failed").asFailure()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.updateAuthRequest(
|
||||
requestId = "requestId",
|
||||
masterPasswordHash = "masterPasswordHash",
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
}
|
||||
assertEquals(AuthRequestResult.Error, result)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `updateAuthRequest should return success when service & sdk return success`() = runTest {
|
||||
val requestId = "requestId"
|
||||
val passwordHash = "masterPasswordHash"
|
||||
val encodedKey = "encodedKey"
|
||||
val responseJson = AuthRequestsResponseJson.AuthRequest(
|
||||
id = requestId,
|
||||
publicKey = PUBLIC_KEY,
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "key",
|
||||
masterPasswordHash = passwordHash,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
)
|
||||
val expected = AuthRequestResult.Success(
|
||||
authRequest = AuthRequest(
|
||||
id = requestId,
|
||||
publicKey = PUBLIC_KEY,
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "key",
|
||||
masterPasswordHash = passwordHash,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
vaultSdkSource.getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
} returns encodedKey.asSuccess()
|
||||
coEvery {
|
||||
authRequestsService.updateAuthRequest(
|
||||
requestId = requestId,
|
||||
masterPasswordHash = passwordHash,
|
||||
key = encodedKey,
|
||||
deviceId = UNIQUE_APP_ID,
|
||||
isApproved = false,
|
||||
)
|
||||
} returns responseJson.asSuccess()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.updateAuthRequest(
|
||||
requestId = requestId,
|
||||
masterPasswordHash = passwordHash,
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getAuthRequestKey(
|
||||
publicKey = PUBLIC_KEY,
|
||||
userId = USER_ID_1,
|
||||
)
|
||||
authRequestsService.updateAuthRequest(
|
||||
requestId = requestId,
|
||||
masterPasswordHash = passwordHash,
|
||||
key = encodedKey,
|
||||
deviceId = UNIQUE_APP_ID,
|
||||
isApproved = false,
|
||||
)
|
||||
}
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getIsKnownDevice should return failure when service returns failure`() = runTest {
|
||||
coEvery {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
|
@ -43,20 +47,6 @@ class LoginApprovalScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `on Confirm login should send ApproveRequestClick`() = runTest {
|
||||
// Set to content state to show appropriate buttons
|
||||
mutableStateFlow.tryEmit(
|
||||
LoginApprovalState(
|
||||
fingerprint = FINGERPRINT,
|
||||
viewState = LoginApprovalState.ViewState.Content(
|
||||
deviceType = "Android",
|
||||
domainUrl = "bitwarden.com",
|
||||
email = "test@bitwarden.com",
|
||||
fingerprint = FINGERPRINT,
|
||||
ipAddress = "1.0.0.1",
|
||||
time = "now",
|
||||
),
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNodeWithText("Confirm login")
|
||||
.performScrollTo()
|
||||
|
@ -69,20 +59,6 @@ class LoginApprovalScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `on Deny login should send DeclineRequestClick`() = runTest {
|
||||
// Set to content state to show appropriate buttons
|
||||
mutableStateFlow.tryEmit(
|
||||
LoginApprovalState(
|
||||
fingerprint = FINGERPRINT,
|
||||
viewState = LoginApprovalState.ViewState.Content(
|
||||
deviceType = "Android",
|
||||
domainUrl = "bitwarden.com",
|
||||
email = "test@bitwarden.com",
|
||||
fingerprint = FINGERPRINT,
|
||||
ipAddress = "1.0.0.1",
|
||||
time = "now",
|
||||
),
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNodeWithText("Deny login")
|
||||
.performScrollTo()
|
||||
|
@ -91,10 +67,43 @@ class LoginApprovalScreenTest : BaseComposeTest() {
|
|||
viewModel.trySendAction(LoginApprovalAction.DeclineRequestClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on error dialog dismiss click should send ErrorDialogDismiss`() = runTest {
|
||||
mutableStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowErrorDialog = true,
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("An error has occurred.")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Ok")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(LoginApprovalAction.ErrorDialogDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val FINGERPRINT = "fingerprint"
|
||||
private val DEFAULT_STATE: LoginApprovalState = LoginApprovalState(
|
||||
fingerprint = FINGERPRINT,
|
||||
viewState = LoginApprovalState.ViewState.Loading,
|
||||
masterPasswordHash = null,
|
||||
publicKey = "publicKey",
|
||||
requestId = "",
|
||||
shouldShowErrorDialog = false,
|
||||
viewState = LoginApprovalState.ViewState.Content(
|
||||
deviceType = "Android",
|
||||
domainUrl = "bitwarden.com",
|
||||
email = "test@bitwarden.com",
|
||||
fingerprint = FINGERPRINT,
|
||||
ipAddress = "1.0.0.1",
|
||||
time = "now",
|
||||
),
|
||||
)
|
||||
|
|
|
@ -8,7 +8,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
|
@ -82,23 +81,72 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on ApproveRequestClick should emit ShowToast`() = runTest {
|
||||
fun `on ApproveRequestClick should approve auth request`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
coEvery {
|
||||
mockAuthRepository.updateAuthRequest(
|
||||
requestId = REQUEST_ID,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = true,
|
||||
)
|
||||
} returns AuthRequestResult.Success(AUTH_REQUEST)
|
||||
|
||||
viewModel.trySendAction(LoginApprovalAction.ApproveRequestClick)
|
||||
assertEquals(LoginApprovalEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
|
||||
coVerify {
|
||||
mockAuthRepository.updateAuthRequest(
|
||||
requestId = REQUEST_ID,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DeclineRequestClick should emit ShowToast`() = runTest {
|
||||
fun `on DeclineRequestClick should deny auth request`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LoginApprovalAction.ApproveRequestClick)
|
||||
assertEquals(LoginApprovalEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
coEvery {
|
||||
mockAuthRepository.updateAuthRequest(
|
||||
requestId = REQUEST_ID,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
} returns AuthRequestResult.Success(AUTH_REQUEST)
|
||||
|
||||
viewModel.trySendAction(LoginApprovalAction.DeclineRequestClick)
|
||||
|
||||
coVerify {
|
||||
mockAuthRepository.updateAuthRequest(
|
||||
requestId = REQUEST_ID,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ErrorDialogDismiss should update state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
coEvery {
|
||||
mockAuthRepository.updateAuthRequest(
|
||||
requestId = REQUEST_ID,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
publicKey = PUBLIC_KEY,
|
||||
isApproved = false,
|
||||
)
|
||||
} returns AuthRequestResult.Error
|
||||
viewModel.trySendAction(LoginApprovalAction.DeclineRequestClick)
|
||||
|
||||
assertEquals(viewModel.stateFlow.value, DEFAULT_STATE.copy(shouldShowErrorDialog = true))
|
||||
viewModel.trySendAction(LoginApprovalAction.ErrorDialogDismiss)
|
||||
|
||||
assertEquals(viewModel.stateFlow.value, DEFAULT_STATE.copy(shouldShowErrorDialog = false))
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
authRepository: AuthRepository = mockAuthRepository,
|
||||
state: LoginApprovalState? = DEFAULT_STATE,
|
||||
|
@ -112,8 +160,15 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private const val EMAIL = "test@bitwarden.com"
|
||||
private const val FINGERPRINT = "fingerprint"
|
||||
private const val PASSWORD_HASH = "verySecureHash"
|
||||
private const val PUBLIC_KEY = "publicKey"
|
||||
private const val REQUEST_ID = "requestId"
|
||||
private val DEFAULT_STATE: LoginApprovalState = LoginApprovalState(
|
||||
fingerprint = FINGERPRINT,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
publicKey = PUBLIC_KEY,
|
||||
requestId = REQUEST_ID,
|
||||
shouldShowErrorDialog = false,
|
||||
viewState = LoginApprovalState.ViewState.Content(
|
||||
deviceType = "Android",
|
||||
domainUrl = "www.bitwarden.com",
|
||||
|
@ -142,12 +197,12 @@ private val DEFAULT_USER_STATE = UserState(
|
|||
),
|
||||
)
|
||||
private val AUTH_REQUEST = AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
id = REQUEST_ID,
|
||||
publicKey = PUBLIC_KEY,
|
||||
platform = "Android",
|
||||
ipAddress = "1.0.0.1",
|
||||
key = "public",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
|
|
|
@ -177,6 +177,75 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `on LifecycleResume should update state`() = runTest {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(emptyList())
|
||||
val viewModel = createViewModel()
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "publicKey-1",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2023-08-24T17:11Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "pantry-overdue-survive-sleep-jab",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "2",
|
||||
publicKey = "publicKey-2",
|
||||
platform = "iOS",
|
||||
ipAddress = "192.168.0.2",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2023-08-21T15:43Z"),
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "erupt-anew-matchbook-disk-student",
|
||||
),
|
||||
),
|
||||
)
|
||||
val expected = DEFAULT_STATE.copy(
|
||||
viewState = PendingRequestsState.ViewState.Content(
|
||||
requests = listOf(
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
|
||||
platform = "Android",
|
||||
timestamp = "8/24/23 05:11 PM",
|
||||
),
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||
platform = "iOS",
|
||||
timestamp = "8/21/23 03:43 PM",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(PendingRequestsAction.LifecycleResume)
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
|
||||
coVerify(exactly = 2) {
|
||||
authRepository.getAuthRequests()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: PendingRequestsState? = DEFAULT_STATE,
|
||||
): PendingRequestsViewModel = PendingRequestsViewModel(
|
||||
|
|
Loading…
Reference in a new issue