mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 01:46:00 +03:00
BIT-1888 Add the check for the last revision date (#1029)
This commit is contained in:
parent
2d54fc4616
commit
c33fc8cf97
8 changed files with 139 additions and 28 deletions
|
@ -77,6 +77,12 @@ interface VaultDiskSource {
|
|||
*/
|
||||
suspend fun replaceVaultData(userId: String, vault: SyncResponseJson)
|
||||
|
||||
/**
|
||||
* Trigger re-emissions from the [getCiphers], [getCollections], [getFolders], and [getSends]
|
||||
* functions.
|
||||
*/
|
||||
suspend fun resyncVaultData(userId: String)
|
||||
|
||||
/**
|
||||
* Deletes all stored vault data from the data source for a given [userId].
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,7 @@ import kotlinx.coroutines.async
|
|||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -264,6 +265,20 @@ class VaultDiskSourceImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun resyncVaultData(userId: String) {
|
||||
coroutineScope {
|
||||
val deferredCiphers = async { getCiphers(userId = userId).first() }
|
||||
val deferredCollections = async { getCollections(userId = userId).first() }
|
||||
val deferredFolders = async { getFolders(userId = userId).first() }
|
||||
val deferredSends = async { getSends(userId = userId).first() }
|
||||
|
||||
forceCiphersFlow.tryEmit(deferredCiphers.await())
|
||||
forceCollectionsFlow.tryEmit(deferredCollections.await())
|
||||
forceFolderFlow.tryEmit(deferredFolders.await())
|
||||
forceSendFlow.tryEmit(deferredSends.await())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteVaultData(userId: String) {
|
||||
coroutineScope {
|
||||
val deferredCiphers = async { ciphersDao.deleteAllCiphers(userId = userId) }
|
||||
|
|
|
@ -14,4 +14,7 @@ interface SyncApi {
|
|||
*/
|
||||
@GET("sync")
|
||||
suspend fun sync(): Result<SyncResponseJson>
|
||||
|
||||
@GET("/accounts/revision-date")
|
||||
suspend fun getAccountRevisionDateMillis(): Result<Long>
|
||||
}
|
||||
|
|
|
@ -10,4 +10,10 @@ interface SyncService {
|
|||
* Make sync request to get vault items.
|
||||
*/
|
||||
suspend fun sync(): Result<SyncResponseJson>
|
||||
|
||||
/**
|
||||
* Make a request to get the most recent revision date for the account that is returned as an
|
||||
* epoch time in milliseconds.
|
||||
*/
|
||||
suspend fun getAccountRevisionDateMillis(): Result<Long>
|
||||
}
|
||||
|
|
|
@ -7,4 +7,7 @@ class SyncServiceImpl(
|
|||
private val syncApi: SyncApi,
|
||||
) : SyncService {
|
||||
override suspend fun sync(): Result<SyncResponseJson> = syncApi.sync()
|
||||
|
||||
override suspend fun getAccountRevisionDateMillis(): Result<Long> =
|
||||
syncApi.getAccountRevisionDateMillis()
|
||||
}
|
||||
|
|
|
@ -317,6 +317,27 @@ class VaultRepositoryImpl(
|
|||
mutableCollectionsStateFlow.updateToPendingOrLoading()
|
||||
mutableSendDataStateFlow.updateToPendingOrLoading()
|
||||
syncJob = ioScope.launch {
|
||||
val lastSyncInstant = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.toEpochMilli()
|
||||
?: 0
|
||||
|
||||
syncService
|
||||
.getAccountRevisionDateMillis()
|
||||
.fold(
|
||||
onSuccess = { serverRevisionDate ->
|
||||
if (serverRevisionDate < lastSyncInstant) {
|
||||
// We can skip the actual sync call if there is no new data
|
||||
vaultDiskSource.resyncVaultData(userId)
|
||||
return@launch
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
updateVaultStateFlowsToError(it)
|
||||
return@launch
|
||||
},
|
||||
)
|
||||
|
||||
syncService
|
||||
.sync()
|
||||
.fold(
|
||||
|
@ -350,31 +371,7 @@ class VaultRepositoryImpl(
|
|||
settingsDiskSource.storeLastSyncTime(userId = userId, clock.instant())
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
mutableCiphersStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableDomainsStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableFoldersStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableCollectionsStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableSendDataStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
updateVaultStateFlowsToError(throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1407,6 +1404,34 @@ class VaultRepositoryImpl(
|
|||
}
|
||||
.onEach { mutableSendDataStateFlow.value = it }
|
||||
|
||||
private fun updateVaultStateFlowsToError(throwable: Throwable) {
|
||||
mutableCiphersStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableDomainsStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableFoldersStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableCollectionsStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
mutableSendDataStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Push notification helpers
|
||||
/**
|
||||
* Deletes the cipher specified by [syncCipherDeleteData] from disk.
|
||||
|
|
|
@ -22,8 +22,21 @@ class SyncServiceTest : BaseServiceTest() {
|
|||
val result = syncService.sync()
|
||||
assertEquals(createMockSyncResponse(number = 1), result.getOrThrow())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `revision date should return the correct response`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(REVISION_DATE_SUCCESS_JSON))
|
||||
val result = syncService.getAccountRevisionDateMillis()
|
||||
assertEquals(
|
||||
REVISION_DATE_MILLISECONDS,
|
||||
result.getOrThrow(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val REVISION_DATE_MILLISECONDS = 1707847670747L
|
||||
private const val REVISION_DATE_SUCCESS_JSON = """1707847670747"""
|
||||
|
||||
private const val SYNC_SUCCESS_JSON = """
|
||||
{
|
||||
"profile": {
|
||||
|
|
|
@ -156,12 +156,22 @@ class VaultRepositoryTest {
|
|||
}
|
||||
private val fileManager: FileManager = mockk()
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val settingsDiskSource = mockk<SettingsDiskSource>()
|
||||
private val syncService: SyncService = mockk()
|
||||
private val settingsDiskSource = mockk<SettingsDiskSource> {
|
||||
every {
|
||||
getLastSyncTime(userId = any())
|
||||
} returns clock.instant()
|
||||
}
|
||||
private val syncService: SyncService = mockk {
|
||||
coEvery {
|
||||
getAccountRevisionDateMillis()
|
||||
} returns clock.instant().plus(1, ChronoUnit.MINUTES).toEpochMilli().asSuccess()
|
||||
}
|
||||
private val sendsService: SendsService = mockk()
|
||||
private val ciphersService: CiphersService = mockk()
|
||||
private val folderService: FolderService = mockk()
|
||||
private val vaultDiskSource: VaultDiskSource = mockk()
|
||||
private val vaultDiskSource: VaultDiskSource = mockk {
|
||||
coEvery { resyncVaultData(any()) } just runs
|
||||
}
|
||||
private val totpCodeManager: TotpCodeManager = mockk()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk {
|
||||
every { clearCrypto(userId = any()) } just runs
|
||||
|
@ -257,6 +267,7 @@ class VaultRepositoryTest {
|
|||
vaultRepository.sync()
|
||||
coVerify(exactly = 2) {
|
||||
// A second sync should have happened now since it was cancelled by the userState change
|
||||
syncService.getAccountRevisionDateMillis()
|
||||
syncService.sync()
|
||||
}
|
||||
}
|
||||
|
@ -933,6 +944,35 @@ class VaultRepositoryTest {
|
|||
coVerify(exactly = 0) { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync when the last sync time is older than the revision date should sync the vault`() {
|
||||
val userId = "mockId-1"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().minus(1, ChronoUnit.MINUTES)
|
||||
|
||||
coEvery { syncService.sync() } just awaits
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
coVerify { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync when the last sync time is more recent than the revision date should not sync `() {
|
||||
val userId = "mockId-1"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { syncService.sync() } just awaits
|
||||
every {
|
||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
} returns clock.instant().plus(2, ChronoUnit.MINUTES)
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lockVaultForCurrentUser should delegate to the VaultLockManager`() {
|
||||
vaultRepository.lockVaultForCurrentUser()
|
||||
|
|
Loading…
Reference in a new issue