mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 01:46:00 +03:00
BIT-1508: Implement decline all pending requests & add filters (#845)
This commit is contained in:
parent
88da5b2007
commit
f2053bbb07
7 changed files with 415 additions and 123 deletions
|
@ -1,5 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
|
@ -17,6 +19,7 @@ import java.time.ZonedDateTime
|
|||
* @param originUrl The origin URL of this auth request.
|
||||
* @param fingerprint The fingerprint of this auth request.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AuthRequest(
|
||||
val id: String,
|
||||
val publicKey: String,
|
||||
|
@ -29,4 +32,4 @@ data class AuthRequest(
|
|||
val requestApproved: Boolean,
|
||||
val originUrl: String,
|
||||
val fingerprint: String,
|
||||
)
|
||||
) : Parcelable
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.ui.platform.base.util
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Returns a [Boolean] indicating whether this [ZonedDateTime] is five or more minutes old.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun ZonedDateTime.isOverFiveMinutesOld(): Boolean =
|
||||
Duration
|
||||
.between(
|
||||
this,
|
||||
ZonedDateTime.now(TimeZone.getDefault().toZoneId()),
|
||||
)
|
||||
.toMinutes() > 5
|
|
@ -25,7 +25,9 @@ import androidx.compose.material3.rememberTopAppBarState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
@ -44,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButtonWith
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||
|
||||
|
@ -79,6 +82,7 @@ fun PendingRequestsScreen(
|
|||
Lifecycle.Event.ON_RESUME -> {
|
||||
viewModel.trySendAction(PendingRequestsAction.LifecycleResume)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
@ -107,10 +111,10 @@ fun PendingRequestsScreen(
|
|||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = viewState,
|
||||
onDeclineAllRequestsClick = remember(viewModel) {
|
||||
onDeclineAllRequestsConfirm = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
PendingRequestsAction.DeclineAllRequestsClick,
|
||||
PendingRequestsAction.DeclineAllRequestsConfirm,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -152,13 +156,31 @@ fun PendingRequestsScreen(
|
|||
@Composable
|
||||
private fun PendingRequestsContent(
|
||||
state: PendingRequestsState.ViewState.Content,
|
||||
onDeclineAllRequestsClick: () -> Unit,
|
||||
onDeclineAllRequestsConfirm: () -> Unit,
|
||||
onNavigateToLoginApproval: (fingerprint: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
var shouldShowDeclineAllRequestsConfirm by remember { mutableStateOf(false) }
|
||||
|
||||
if (shouldShowDeclineAllRequestsConfirm) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(R.string.decline_all_requests),
|
||||
message = stringResource(
|
||||
id = R.string.are_you_sure_you_want_to_decline_all_pending_log_in_requests,
|
||||
),
|
||||
confirmButtonText = stringResource(R.string.yes),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = {
|
||||
onDeclineAllRequestsConfirm()
|
||||
shouldShowDeclineAllRequestsConfirm = false
|
||||
},
|
||||
onDismissClick = { shouldShowDeclineAllRequestsConfirm = false },
|
||||
onDismissRequest = { shouldShowDeclineAllRequestsConfirm = false },
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
Modifier.padding(bottom = 16.dp),
|
||||
|
@ -181,7 +203,7 @@ private fun PendingRequestsContent(
|
|||
BitwardenFilledTonalButtonWithIcon(
|
||||
label = stringResource(id = R.string.decline_all_requests),
|
||||
icon = painterResource(id = R.drawable.ic_trash),
|
||||
onClick = onDeclineAllRequestsClick,
|
||||
onClick = { shouldShowDeclineAllRequestsConfirm = true },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
|
|
|
@ -4,10 +4,11 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.AuthRequestsResult
|
||||
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.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.isOverFiveMinutesOld
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -27,6 +28,7 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<PendingRequestsState, PendingRequestsEvent, PendingRequestsAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: PendingRequestsState(
|
||||
authRequests = emptyList(),
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
),
|
||||
) {
|
||||
|
@ -42,7 +44,7 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
override fun handleAction(action: PendingRequestsAction) {
|
||||
when (action) {
|
||||
PendingRequestsAction.CloseClick -> handleCloseClicked()
|
||||
PendingRequestsAction.DeclineAllRequestsClick -> handleDeclineAllRequestsClicked()
|
||||
PendingRequestsAction.DeclineAllRequestsConfirm -> handleDeclineAllRequestsConfirmed()
|
||||
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
|
||||
is PendingRequestsAction.PendingRequestRowClick -> {
|
||||
handlePendingRequestRowClicked(action)
|
||||
|
@ -58,8 +60,23 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
sendEvent(PendingRequestsEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleDeclineAllRequestsClicked() {
|
||||
sendEvent(PendingRequestsEvent.ShowToast("Not yet implemented.".asText()))
|
||||
private fun handleDeclineAllRequestsConfirmed() {
|
||||
viewModelScope.launch {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
)
|
||||
}
|
||||
mutableStateFlow.value.authRequests.forEach { request ->
|
||||
authRepository.updateAuthRequest(
|
||||
requestId = request.id,
|
||||
masterPasswordHash = request.masterPasswordHash,
|
||||
publicKey = request.publicKey,
|
||||
isApproved = false,
|
||||
)
|
||||
}
|
||||
updateAuthRequestList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnLifecycleResumed() {
|
||||
|
@ -75,30 +92,48 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
private fun handleAuthRequestsResultReceived(
|
||||
action: PendingRequestsAction.Internal.AuthRequestsResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = when (val result = action.authRequestsResult) {
|
||||
is AuthRequestsResult.Success -> {
|
||||
if (result.authRequests.isEmpty()) {
|
||||
PendingRequestsState.ViewState.Empty
|
||||
} else {
|
||||
PendingRequestsState.ViewState.Content(
|
||||
requests = result.authRequests.map { authRequest ->
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = authRequest.fingerprint,
|
||||
platform = authRequest.platform,
|
||||
timestamp = dateTimeFormatter.format(
|
||||
authRequest.creationDate,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
when (val result = action.authRequestsResult) {
|
||||
is AuthRequestsResult.Success -> {
|
||||
val requests = result
|
||||
.authRequests
|
||||
.filterRespondedAndExpired()
|
||||
.sortedByDescending { request -> request.creationDate }
|
||||
.map { request ->
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = request.fingerprint,
|
||||
platform = request.platform,
|
||||
timestamp = dateTimeFormatter.format(
|
||||
request.creationDate,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (requests.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
authRequests = emptyList(),
|
||||
viewState = PendingRequestsState.ViewState.Empty,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
authRequests = result.authRequests,
|
||||
viewState = PendingRequestsState.ViewState.Content(
|
||||
requests = requests,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuthRequestsResult.Error -> PendingRequestsState.ViewState.Error
|
||||
},
|
||||
)
|
||||
AuthRequestsResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
authRequests = emptyList(),
|
||||
viewState = PendingRequestsState.ViewState.Error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +154,7 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
*/
|
||||
@Parcelize
|
||||
data class PendingRequestsState(
|
||||
val authRequests: List<AuthRequest>,
|
||||
val viewState: ViewState,
|
||||
) : Parcelable {
|
||||
/**
|
||||
|
@ -201,9 +237,9 @@ sealed class PendingRequestsAction {
|
|||
data object CloseClick : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The user has clicked to deny all login requests.
|
||||
* The user has confirmed they want to deny all login requests.
|
||||
*/
|
||||
data object DeclineAllRequestsClick : PendingRequestsAction()
|
||||
data object DeclineAllRequestsConfirm : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The screen has been re-opened and should be updated.
|
||||
|
@ -229,3 +265,16 @@ sealed class PendingRequestsAction {
|
|||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out [AuthRequest]s that match one of the following criteria:
|
||||
* * The request has been approved.
|
||||
* * The request has been declined (indicated by it not being approved & having a responseDate).
|
||||
* * The request has expired (it is at least 5 minutes old).
|
||||
*/
|
||||
private fun List<AuthRequest>.filterRespondedAndExpired() =
|
||||
filterNot { request ->
|
||||
request.requestApproved ||
|
||||
request.responseDate != null ||
|
||||
request.creationDate.isOverFiveMinutesOld()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package com.x8bit.bitwarden.data.platform.base.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.isOverFiveMinutesOld
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TimeZone
|
||||
|
||||
class ZonedDateTimeExtensionsTest {
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
TimeZone.setDefault(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns true when time is old`() {
|
||||
val time = ZonedDateTime.parse("2022-09-13T00:00Z")
|
||||
assertTrue(time.isOverFiveMinutesOld())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns false when time is now`() {
|
||||
val time = ZonedDateTime.now()
|
||||
assertFalse(time.isOverFiveMinutesOld())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns false when time is now minus 5 minutes`() {
|
||||
val time = ZonedDateTime.now().minusMinutes(5)
|
||||
assertFalse(time.isOverFiveMinutesOld())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns true when time is now minus 6 minutes`() {
|
||||
val time = ZonedDateTime.now().minusMinutes(6)
|
||||
assertTrue(time.isOverFiveMinutesOld())
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
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 com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
@ -49,34 +53,72 @@ class PendingRequestsScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on DeclineAllRequestsClick should send DeclineAllRequestsClick`() = runTest {
|
||||
fun `on decline all requests confirmation should send DeclineAllRequestsConfirm`() = runTest {
|
||||
// set content so the Decline all requests button appears
|
||||
mutableStateFlow.tryEmit(
|
||||
PendingRequestsState(
|
||||
viewState = PendingRequestsState.ViewState.Content(
|
||||
requests = listOf(
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
|
||||
platform = "iOS",
|
||||
timestamp = "8/24/2023 11:11 AM",
|
||||
),
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||
platform = "Android",
|
||||
timestamp = "8/21/2023 9:43 AM",
|
||||
),
|
||||
mutableStateFlow.value = PendingRequestsState(
|
||||
authRequests = listOf(),
|
||||
viewState = PendingRequestsState.ViewState.Content(
|
||||
requests = listOf(
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
|
||||
platform = "iOS",
|
||||
timestamp = "8/24/2023 11:11 AM",
|
||||
),
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||
platform = "Android",
|
||||
timestamp = "8/21/2023 9:43 AM",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
composeTestRule.onNodeWithText("Decline all requests").performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Yes")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(PendingRequestsAction.DeclineAllRequestsClick)
|
||||
viewModel.trySendAction(PendingRequestsAction.DeclineAllRequestsConfirm)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on decline all requests cancel should hide confirmation dialog`() = runTest {
|
||||
// set content so the Decline all requests button appears
|
||||
mutableStateFlow.value = PendingRequestsState(
|
||||
authRequests = listOf(),
|
||||
viewState = PendingRequestsState.ViewState.Content(
|
||||
requests = listOf(
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
|
||||
platform = "iOS",
|
||||
timestamp = "8/24/2023 11:11 AM",
|
||||
),
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||
platform = "Android",
|
||||
timestamp = "8/21/2023 9:43 AM",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
composeTestRule.onNodeWithText("Decline all requests").performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Cancel")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(PendingRequestsAction.DeclineAllRequestsConfirm)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
|
||||
authRequests = emptyList(),
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
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.AuthRequestsResult
|
||||
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.mockk
|
||||
|
@ -16,6 +16,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.TimeZone
|
||||
|
||||
class PendingRequestsViewModelTest : BaseViewModelTest() {
|
||||
|
@ -48,52 +49,80 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `getPendingResults success with content should update state`() {
|
||||
fun `getPendingResults success with content should update state with some requests filtered`() {
|
||||
val dateTimeFormatter = DateTimeFormatter
|
||||
.ofPattern("M/d/yy hh:mm a")
|
||||
.withZone(TimeZone.getDefault().toZoneId())
|
||||
val nowZonedDateTime = ZonedDateTime.now()
|
||||
val requestList = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "publicKey-1",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = nowZonedDateTime,
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "pantry-overdue-survive-sleep-jab",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "2",
|
||||
publicKey = "publicKey-2",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = nowZonedDateTime,
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "fingerprint",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "3",
|
||||
publicKey = "publicKey-3",
|
||||
platform = "iOS",
|
||||
ipAddress = "192.168.0.2",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.now().minusMinutes(10),
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "erupt-anew-matchbook-disk-student",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "4",
|
||||
publicKey = "publicKey-4",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = nowZonedDateTime,
|
||||
responseDate = nowZonedDateTime,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "fingerprint",
|
||||
),
|
||||
)
|
||||
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",
|
||||
),
|
||||
),
|
||||
authRequests = requestList,
|
||||
)
|
||||
val expected = DEFAULT_STATE.copy(
|
||||
authRequests = requestList,
|
||||
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",
|
||||
timestamp = nowZonedDateTime.format(dateTimeFormatter),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -159,20 +188,82 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `on DeclineAllRequestsClick should send ShowToast event`() = runTest {
|
||||
fun `on DeclineAllRequestsConfirm should update all auth requests to declined`() = runTest {
|
||||
val authRequest1 = AuthRequest(
|
||||
id = "2",
|
||||
publicKey = "publicKey-2",
|
||||
platform = "iOS",
|
||||
ipAddress = "192.168.0.2",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.now().minusMinutes(5),
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "erupt-anew-matchbook-disk-student",
|
||||
)
|
||||
val authRequest2 = AuthRequest(
|
||||
id = "3",
|
||||
publicKey = "publicKey-3",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.3",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.now(),
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "pantry-overdue-survive-sleep-jab",
|
||||
)
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = emptyList(),
|
||||
authRequests = listOf(
|
||||
authRequest1,
|
||||
authRequest2,
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authRepository.updateAuthRequest(
|
||||
requestId = "2",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
publicKey = "publicKey-2",
|
||||
isApproved = false,
|
||||
)
|
||||
} returns AuthRequestResult.Success(
|
||||
authRequest1.copy(
|
||||
responseDate = ZonedDateTime.now(),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authRepository.updateAuthRequest(
|
||||
requestId = "3",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
publicKey = "publicKey-3",
|
||||
isApproved = false,
|
||||
)
|
||||
} returns AuthRequestResult.Success(
|
||||
authRequest2.copy(
|
||||
responseDate = ZonedDateTime.now(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(PendingRequestsAction.DeclineAllRequestsClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
PendingRequestsEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
viewModel.actionChannel.trySend(PendingRequestsAction.DeclineAllRequestsConfirm)
|
||||
|
||||
coVerify {
|
||||
authRepository.updateAuthRequest(
|
||||
requestId = "2",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
publicKey = "publicKey-2",
|
||||
isApproved = false,
|
||||
)
|
||||
authRepository.updateAuthRequest(
|
||||
requestId = "3",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
publicKey = "publicKey-3",
|
||||
isApproved = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +271,53 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
@Suppress("LongMethod")
|
||||
@Test
|
||||
fun `on LifecycleResume should update state`() = runTest {
|
||||
val dateTimeFormatter = DateTimeFormatter
|
||||
.ofPattern("M/d/yy hh:mm a")
|
||||
.withZone(TimeZone.getDefault().toZoneId())
|
||||
val nowZonedDateTime = ZonedDateTime.now()
|
||||
val fiveMinZonedDateTime = ZonedDateTime.now().minusMinutes(5)
|
||||
val sixMinZonedDateTime = ZonedDateTime.now().minusMinutes(6)
|
||||
val requestList = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "publicKey-1",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = sixMinZonedDateTime,
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "fingerprint",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "2",
|
||||
publicKey = "publicKey-2",
|
||||
platform = "iOS",
|
||||
ipAddress = "192.168.0.2",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = fiveMinZonedDateTime,
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "erupt-anew-matchbook-disk-student",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "3",
|
||||
publicKey = "publicKey-3",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.3",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = nowZonedDateTime,
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "pantry-overdue-survive-sleep-jab",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(emptyList())
|
||||
|
@ -193,47 +331,21 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
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",
|
||||
),
|
||||
),
|
||||
authRequests = requestList,
|
||||
)
|
||||
val expected = DEFAULT_STATE.copy(
|
||||
authRequests = requestList,
|
||||
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",
|
||||
timestamp = nowZonedDateTime.format(dateTimeFormatter),
|
||||
),
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||
platform = "iOS",
|
||||
timestamp = "8/21/23 03:43 PM",
|
||||
timestamp = fiveMinZonedDateTime.format(dateTimeFormatter),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -255,6 +367,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
|
||||
companion object {
|
||||
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
|
||||
authRequests = emptyList(),
|
||||
viewState = PendingRequestsState.ViewState.Empty,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue