PM-25642: Force sync or clear last sync time on sync notification (#5958)

This commit is contained in:
David Perez 2025-09-29 14:45:56 -05:00 committed by GitHub
parent df63bb4b6c
commit a02a84ee08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 63 additions and 28 deletions

View File

@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
*/
interface PushManager {
/**
* Flow that represents requests intended for full syncs.
* Flow that represents requests intended for full syncs for the user ID provided.
*/
val fullSyncFlow: Flow<Unit>
val fullSyncFlow: Flow<String>
/**
* Flow that represents requests intended to log a user out.

View File

@ -55,7 +55,7 @@ class PushManagerImpl @Inject constructor(
private val ioScope = CoroutineScope(dispatcherManager.io)
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
private val mutablePasswordlessRequestSharedFlow =
bufferedMutableSharedFlow<PasswordlessRequestData>()
@ -73,7 +73,7 @@ class PushManagerImpl @Inject constructor(
private val mutableSyncSendUpsertSharedFlow =
bufferedMutableSharedFlow<SyncSendUpsertData>()
override val fullSyncFlow: SharedFlow<Unit>
override val fullSyncFlow: SharedFlow<String>
get() = mutableFullSyncSharedFlow.asSharedFlow()
override val logoutFlow: SharedFlow<NotificationLogoutData>
@ -204,7 +204,10 @@ class PushManagerImpl @Inject constructor(
NotificationType.SYNC_SETTINGS,
NotificationType.SYNC_VAULT,
-> {
mutableFullSyncSharedFlow.tryEmit(Unit)
json
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
.userId
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
}
NotificationType.SYNC_FOLDER_CREATE,

View File

@ -83,4 +83,12 @@ sealed class NotificationPayload {
@JsonNames("UserId", "userId") override val userId: String?,
@JsonNames("Id", "id") val loginRequestId: String?,
) : NotificationPayload()
/**
* A notification payload for syncing a users vault.
*/
@Serializable
data class SyncNotification(
@JsonNames("UserId", "userId") override val userId: String?,
) : NotificationPayload()
}

View File

@ -209,7 +209,13 @@ class VaultSyncManagerImpl(
pushManager
.fullSyncFlow
.onEach { sync(forced = false) }
.onEach { userId ->
if (userId == activeUserId) {
sync(forced = false)
} else {
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
}
}
.launchIn(unconfinedScope)
databaseSchemeManager

View File

@ -205,7 +205,7 @@ class ImportLoginsViewModel @Inject constructor(
private fun syncVault() {
viewModelScope.launch {
val result = vaultRepository.syncForResult()
val result = vaultRepository.syncForResult(forced = true)
sendAction(ImportLoginsAction.Internal.VaultSyncResultReceived(result))
}
}

View File

@ -161,7 +161,7 @@ class PushManagerTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
assertEquals(
Unit,
"078966a2-93c2-4618-ae2a-0a2394c88d37",
awaitItem(),
)
}
@ -180,7 +180,7 @@ class PushManagerTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
assertEquals(
Unit,
"078966a2-93c2-4618-ae2a-0a2394c88d37",
awaitItem(),
)
}
@ -191,7 +191,7 @@ class PushManagerTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
assertEquals(
Unit,
"078966a2-93c2-4618-ae2a-0a2394c88d37",
awaitItem(),
)
}
@ -580,7 +580,7 @@ class PushManagerTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
assertEquals(
Unit,
"078966a2-93c2-4618-ae2a-0a2394c88d37",
awaitItem(),
)
}
@ -599,7 +599,7 @@ class PushManagerTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
assertEquals(
Unit,
"078966a2-93c2-4618-ae2a-0a2394c88d37",
awaitItem(),
)
}
@ -610,7 +610,7 @@ class PushManagerTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
assertEquals(
Unit,
"078966a2-93c2-4618-ae2a-0a2394c88d37",
awaitItem(),
)
}

View File

@ -126,7 +126,7 @@ class VaultSyncManagerTest {
private val userLogoutManager: UserLogoutManager = mockk {
every { softLogout(any(), any()) } just runs
}
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
private val mutableFullSyncFlow = bufferedMutableSharedFlow<String>()
private val pushManager: PushManager = mockk {
every { fullSyncFlow } returns mutableFullSyncFlow
}
@ -1105,15 +1105,27 @@ class VaultSyncManagerTest {
}
@Test
fun `fullSyncFlow emission should trigger unforced sync`() {
fun `fullSyncFlow emission with active user ID should trigger an unforced sync`() {
val userId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
every { settingsDiskSource.getLastSyncTime(userId = userId) } returns null
coEvery { syncService.sync() } just awaits
mutableFullSyncFlow.tryEmit(Unit)
mutableFullSyncFlow.tryEmit(userId)
coVerify { syncService.sync() }
coVerify(exactly = 1) { syncService.sync() }
}
@Suppress("MaxLineLength")
@Test
fun `fullSyncFlow emission with non-active user ID should clear last sync time for that user`() {
val userId = "mockId-2"
fakeAuthDiskSource.userState = MOCK_USER_STATE
mutableFullSyncFlow.tryEmit(userId)
coVerify(exactly = 1) {
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
}
}
@Suppress("MaxLineLength")

View File

@ -33,7 +33,9 @@ import org.junit.jupiter.api.Test
class ImportLoginsViewModelTest : BaseViewModelTest() {
private val vaultRepository: VaultRepository = mockk {
coEvery { syncForResult() } returns SyncVaultDataResult.Success(itemsAvailable = true)
coEvery {
syncForResult(forced = true)
} returns SyncVaultDataResult.Success(itemsAvailable = true)
}
private val firstTimeActionManager: FirstTimeActionManager = mockk {
@ -312,21 +314,23 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
)
cancelAndIgnoreRemainingEvents()
}
coVerify { vaultRepository.syncForResult() }
coVerify(exactly = 1) { vaultRepository.syncForResult(forced = true) }
}
@Suppress("MaxLineLength")
@Test
fun `RetryVaultSync sets isVaultSyncing to true and clears dialog state and calls syncForResult`() =
runTest {
coEvery { vaultRepository.syncForResult() } returns SyncVaultDataResult.Error(Exception())
coEvery {
vaultRepository.syncForResult(forced = true)
} returns SyncVaultDataResult.Error(Exception())
val viewModel = createViewModel()
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
viewModel.stateFlow.test {
assertNotNull(awaitItem().dialogState)
coEvery { vaultRepository.syncForResult() } returns SyncVaultDataResult.Success(
itemsAvailable = true,
)
coEvery {
vaultRepository.syncForResult(forced = true)
} returns SyncVaultDataResult.Success(itemsAvailable = true)
viewModel.trySendAction(ImportLoginsAction.RetryVaultSync)
assertEquals(
ImportLoginsState(
@ -339,7 +343,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
)
cancelAndIgnoreRemainingEvents()
}
coVerify { vaultRepository.syncForResult() }
coVerify(exactly = 2) { vaultRepository.syncForResult(forced = true) }
}
@Suppress("MaxLineLength")
@ -367,7 +371,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
fun `MoveToSyncInProgress should set no items imported error dialog state when sync succeeds but no items are available`() =
runTest {
coEvery {
vaultRepository.syncForResult()
vaultRepository.syncForResult(forced = true)
} returns SyncVaultDataResult.Success(itemsAvailable = false)
val viewModel = createViewModel()
viewModel.stateFlow.test {
@ -402,7 +406,9 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
@Test
fun `SyncVaultDataResult Error should remove loading state and show error dialog`() = runTest {
coEvery { vaultRepository.syncForResult() } returns SyncVaultDataResult.Error(Exception())
coEvery {
vaultRepository.syncForResult(forced = true)
} returns SyncVaultDataResult.Error(Exception())
val viewModel = createViewModel()
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
assertEquals(
@ -420,7 +426,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
fun `FailSyncAcknowledged should remove dialog state and send NavigateBack event`() =
runTest {
coEvery {
vaultRepository.syncForResult()
vaultRepository.syncForResult(forced = true)
} returns SyncVaultDataResult.Error(Exception())
val viewModel = createViewModel()
viewModel.eventFlow.test {