Polling on pending requests screen (#942)

This commit is contained in:
David Perez 2024-02-01 09:42:40 -06:00 committed by Álison Fernandes
parent 10cf094c3f
commit 46bc489f1f
6 changed files with 119 additions and 49 deletions

View file

@ -7,6 +7,7 @@ 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.AuthRequestUpdatesResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.CreateAuthRequestResult
@ -237,6 +238,11 @@ interface AuthRepository : AuthenticatorProvider {
*/
fun getAuthRequestByIdFlow(requestId: String): Flow<AuthRequestUpdatesResult>
/**
* Get all auth request and emits updates over time.
*/
fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult>
/**
* Get a list of the current user's [AuthRequest]s.
*/

View file

@ -36,6 +36,7 @@ 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.AuthRequestUpdatesResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.CreateAuthRequestResult
@ -824,6 +825,19 @@ class AuthRepositoryImpl(
mutableSsoCallbackResultFlow.tryEmit(result)
}
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
while (currentCoroutineContext().isActive) {
when (val result = getAuthRequests()) {
AuthRequestsResult.Error -> emit(AuthRequestsUpdatesResult.Error)
is AuthRequestsResult.Success -> {
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
}
}
delay(timeMillis = PASSWORDLESS_APPROVER_INTERVAL_MILLIS)
}
}
@Suppress("LongMethod")
override fun createAuthRequestWithUpdates(
email: String,

View file

@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of an authorization approval request.
*/
sealed class AuthRequestsUpdatesResult {
/**
* Models the data returned when creating an auth request.
*/
data class Update(
val authRequests: List<AuthRequest>,
) : AuthRequestsUpdatesResult()
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestsUpdatesResult()
}

View file

@ -5,12 +5,13 @@ 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.data.auth.repository.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
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.isOverFiveMinutesOld
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -38,6 +39,8 @@ class PendingRequestsViewModel @Inject constructor(
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
),
) {
private var authJob: Job = Job().apply { complete() }
private val dateTimeFormatter
get() = DateTimeFormatter
.ofPattern("M/d/yy hh:mm a")
@ -126,8 +129,8 @@ class PendingRequestsViewModel @Inject constructor(
private fun handleAuthRequestsResultReceived(
action: PendingRequestsAction.Internal.AuthRequestsResultReceive,
) {
when (val result = action.authRequestsResult) {
is AuthRequestsResult.Success -> {
when (val result = action.authRequestsUpdatesResult) {
is AuthRequestsUpdatesResult.Update -> {
val requests = result
.authRequests
.filterRespondedAndExpired()
@ -160,7 +163,7 @@ class PendingRequestsViewModel @Inject constructor(
}
}
AuthRequestsResult.Error -> {
AuthRequestsUpdatesResult.Error -> {
mutableStateFlow.update {
it.copy(
authRequests = emptyList(),
@ -173,13 +176,12 @@ class PendingRequestsViewModel @Inject constructor(
}
private fun updateAuthRequestList() {
viewModelScope.launch {
trySendAction(
PendingRequestsAction.Internal.AuthRequestsResultReceive(
authRequestsResult = authRepository.getAuthRequests(),
),
)
}
authJob.cancel()
authJob = authRepository
.getAuthRequestsWithUpdates()
.map { PendingRequestsAction.Internal.AuthRequestsResultReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
}
@ -332,7 +334,7 @@ sealed class PendingRequestsAction {
* Indicates that a new auth requests result has been received.
*/
data class AuthRequestsResultReceive(
val authRequestsResult: AuthRequestsResult,
val authRequestsUpdatesResult: AuthRequestsUpdatesResult,
) : Internal()
}
}

View file

@ -46,6 +46,7 @@ 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.AuthRequestUpdatesResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.CreateAuthRequestResult
@ -97,7 +98,9 @@ import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.buildJsonObject
@ -3496,6 +3499,46 @@ class AuthRepositoryTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("MaxLineLength")
@Test
fun `getAuthRequestsWithUpdates should emit error then success and not cancel flow when getAuthRequests fails then succeeds`() =
runTest {
val threeMinutes = 3L * 60L * 1_000L
val authRequests = listOf(AUTH_REQUEST)
val authRequestsResponseJson = AuthRequestsResponseJson(
authRequests = listOf(AUTH_REQUESTS_RESPONSE_JSON_AUTH_RESPONSE),
)
val expectedOne = AuthRequestsUpdatesResult.Error
val expectedTwo = AuthRequestsUpdatesResult.Update(authRequests = authRequests)
coEvery {
authRequestsService.getAuthRequests()
} returns Throwable("Fail").asFailure() andThen authRequestsResponseJson.asSuccess()
coEvery {
authSdkSource.getUserFingerprint(
email = EMAIL,
publicKey = PUBLIC_KEY,
)
} returns Result.success(FINGER_PRINT)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
repository
.getAuthRequestsWithUpdates()
.test {
assertEquals(expectedOne, awaitItem())
advanceTimeBy(threeMinutes)
expectNoEvents()
advanceTimeBy(threeMinutes)
assertEquals(expectedTwo, awaitItem())
advanceTimeBy(threeMinutes)
cancelAndIgnoreRemainingEvents()
}
coVerify(exactly = 2) {
authRequestsService.getAuthRequests()
}
}
@Test
fun `getAuthRequests should return failure when service returns failure`() = runTest {
coEvery {

View file

@ -6,13 +6,13 @@ 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.data.auth.repository.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.awaits
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
@ -26,9 +26,11 @@ import java.util.TimeZone
class PendingRequestsViewModelTest : BaseViewModelTest() {
private val mutableAuthRequestsWithUpdatesFlow =
bufferedMutableSharedFlow<AuthRequestsUpdatesResult>()
private val authRepository = mockk<AuthRepository> {
// This is called during init, anything that cares about this will handle it
coEvery { getAuthRequests() } just awaits
coEvery { getAuthRequestsWithUpdates() } returns mutableAuthRequestsWithUpdatesFlow
}
private val mutablePullToRefreshStateFlow = MutableStateFlow(false)
private val settingsRepository = mockk<SettingsRepository> {
@ -48,16 +50,13 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
}
@Test
fun `initial state should be correct and trigger a getAuthRequests call`() {
coEvery {
authRepository.getAuthRequests()
} returns AuthRequestsResult.Success(
authRequests = emptyList(),
fun `init should call getAuthRequestsWithUpdates`() {
createViewModel(state = null)
mutableAuthRequestsWithUpdatesFlow.tryEmit(
AuthRequestsUpdatesResult.Update(authRequests = emptyList()),
)
val viewModel = createViewModel(state = null)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
coVerify {
authRepository.getAuthRequests()
authRepository.getAuthRequestsWithUpdates()
}
}
@ -122,11 +121,6 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
fingerprint = "fingerprint",
),
)
coEvery {
authRepository.getAuthRequests()
} returns AuthRequestsResult.Success(
authRequests = requestList,
)
val expected = DEFAULT_STATE.copy(
authRequests = requestList,
viewState = PendingRequestsState.ViewState.Content(
@ -140,6 +134,9 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
),
)
val viewModel = createViewModel()
mutableAuthRequestsWithUpdatesFlow.tryEmit(
AuthRequestsUpdatesResult.Update(authRequests = requestList),
)
assertEquals(expected, viewModel.stateFlow.value)
}
@ -159,13 +156,11 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
@Test
fun `getPendingResults failure with error should update state`() {
coEvery {
authRepository.getAuthRequests()
} returns AuthRequestsResult.Error
val expected = DEFAULT_STATE.copy(
viewState = PendingRequestsState.ViewState.Error,
)
val viewModel = createViewModel()
mutableAuthRequestsWithUpdatesFlow.tryEmit(AuthRequestsUpdatesResult.Error)
assertEquals(expected, viewModel.stateFlow.value)
}
@ -184,7 +179,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(PendingRequestsAction.RefreshPull)
coVerify(exactly = 2) {
// This should be called twice since we also call it on init
authRepository.getAuthRequests()
authRepository.getAuthRequestsWithUpdates()
}
}
@ -242,14 +237,6 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
originUrl = "www.bitwarden.com",
fingerprint = "pantry-overdue-survive-sleep-jab",
)
coEvery {
authRepository.getAuthRequests()
} returns AuthRequestsResult.Success(
authRequests = listOf(
authRequest1,
authRequest2,
),
)
coEvery {
authRepository.updateAuthRequest(
requestId = "2",
@ -275,6 +262,9 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
),
)
val viewModel = createViewModel()
mutableAuthRequestsWithUpdatesFlow.tryEmit(
AuthRequestsUpdatesResult.Update(authRequests = listOf(authRequest1, authRequest2)),
)
viewModel.actionChannel.trySend(PendingRequestsAction.DeclineAllRequestsConfirm)
coVerify {
@ -343,20 +333,17 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
fingerprint = "pantry-overdue-survive-sleep-jab",
),
)
coEvery {
authRepository.getAuthRequests()
} returns AuthRequestsResult.Success(emptyList())
val viewModel = createViewModel()
mutableAuthRequestsWithUpdatesFlow.tryEmit(
AuthRequestsUpdatesResult.Update(authRequests = emptyList()),
)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
coEvery {
authRepository.getAuthRequests()
} returns AuthRequestsResult.Success(
authRequests = requestList,
mutableAuthRequestsWithUpdatesFlow.tryEmit(
AuthRequestsUpdatesResult.Update(authRequests = requestList),
)
val expected = DEFAULT_STATE.copy(
authRequests = requestList,
@ -379,7 +366,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
assertEquals(expected, viewModel.stateFlow.value)
coVerify(exactly = 2) {
authRepository.getAuthRequests()
authRepository.getAuthRequestsWithUpdates()
}
}