mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Polling on pending requests screen (#942)
This commit is contained in:
parent
10cf094c3f
commit
46bc489f1f
6 changed files with 119 additions and 49 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue