BIT-1888 Add the check for the last revision date (#1029)

This commit is contained in:
Oleg Semenenko 2024-02-19 08:34:13 -06:00 committed by Álison Fernandes
parent 2d54fc4616
commit c33fc8cf97
8 changed files with 139 additions and 28 deletions

View file

@ -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].
*/

View file

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

View file

@ -14,4 +14,7 @@ interface SyncApi {
*/
@GET("sync")
suspend fun sync(): Result<SyncResponseJson>
@GET("/accounts/revision-date")
suspend fun getAccountRevisionDateMillis(): Result<Long>
}

View file

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

View file

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

View file

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

View file

@ -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": {

View file

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