BIT-1508: Implement decline all pending requests & add filters (#845)

This commit is contained in:
Caleb Derosier 2024-01-29 18:48:45 -07:00 committed by Álison Fernandes
parent 88da5b2007
commit f2053bbb07
7 changed files with 415 additions and 123 deletions

View file

@ -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

View file

@ -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

View file

@ -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(),

View file

@ -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()
}

View file

@ -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())
}
}

View file

@ -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,
)
}

View file

@ -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,
)
}