mirror of
https://github.com/bitwarden/android.git
synced 2025-12-12 08:40:49 -06:00
PM-25642: Force sync or clear last sync time on sync notification (#5958)
This commit is contained in:
parent
df63bb4b6c
commit
a02a84ee08
@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
*/
|
*/
|
||||||
interface PushManager {
|
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.
|
* Flow that represents requests intended to log a user out.
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class PushManagerImpl @Inject constructor(
|
|||||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||||
|
|
||||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
|
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
|
||||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
||||||
private val mutablePasswordlessRequestSharedFlow =
|
private val mutablePasswordlessRequestSharedFlow =
|
||||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||||
@ -73,7 +73,7 @@ class PushManagerImpl @Inject constructor(
|
|||||||
private val mutableSyncSendUpsertSharedFlow =
|
private val mutableSyncSendUpsertSharedFlow =
|
||||||
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||||
|
|
||||||
override val fullSyncFlow: SharedFlow<Unit>
|
override val fullSyncFlow: SharedFlow<String>
|
||||||
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
||||||
|
|
||||||
override val logoutFlow: SharedFlow<NotificationLogoutData>
|
override val logoutFlow: SharedFlow<NotificationLogoutData>
|
||||||
@ -204,7 +204,10 @@ class PushManagerImpl @Inject constructor(
|
|||||||
NotificationType.SYNC_SETTINGS,
|
NotificationType.SYNC_SETTINGS,
|
||||||
NotificationType.SYNC_VAULT,
|
NotificationType.SYNC_VAULT,
|
||||||
-> {
|
-> {
|
||||||
mutableFullSyncSharedFlow.tryEmit(Unit)
|
json
|
||||||
|
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
|
||||||
|
.userId
|
||||||
|
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationType.SYNC_FOLDER_CREATE,
|
NotificationType.SYNC_FOLDER_CREATE,
|
||||||
|
|||||||
@ -83,4 +83,12 @@ sealed class NotificationPayload {
|
|||||||
@JsonNames("UserId", "userId") override val userId: String?,
|
@JsonNames("UserId", "userId") override val userId: String?,
|
||||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||||
) : NotificationPayload()
|
) : NotificationPayload()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A notification payload for syncing a users vault.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class SyncNotification(
|
||||||
|
@JsonNames("UserId", "userId") override val userId: String?,
|
||||||
|
) : NotificationPayload()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,7 +209,13 @@ class VaultSyncManagerImpl(
|
|||||||
|
|
||||||
pushManager
|
pushManager
|
||||||
.fullSyncFlow
|
.fullSyncFlow
|
||||||
.onEach { sync(forced = false) }
|
.onEach { userId ->
|
||||||
|
if (userId == activeUserId) {
|
||||||
|
sync(forced = false)
|
||||||
|
} else {
|
||||||
|
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
|
|
||||||
databaseSchemeManager
|
databaseSchemeManager
|
||||||
|
|||||||
@ -205,7 +205,7 @@ class ImportLoginsViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun syncVault() {
|
private fun syncVault() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = vaultRepository.syncForResult()
|
val result = vaultRepository.syncForResult(forced = true)
|
||||||
sendAction(ImportLoginsAction.Internal.VaultSyncResultReceived(result))
|
sendAction(ImportLoginsAction.Internal.VaultSyncResultReceived(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,7 +161,7 @@ class PushManagerTest {
|
|||||||
pushManager.fullSyncFlow.test {
|
pushManager.fullSyncFlow.test {
|
||||||
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
|
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Unit,
|
"078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ class PushManagerTest {
|
|||||||
pushManager.fullSyncFlow.test {
|
pushManager.fullSyncFlow.test {
|
||||||
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
|
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Unit,
|
"078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -191,7 +191,7 @@ class PushManagerTest {
|
|||||||
pushManager.fullSyncFlow.test {
|
pushManager.fullSyncFlow.test {
|
||||||
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
|
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Unit,
|
"078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -580,7 +580,7 @@ class PushManagerTest {
|
|||||||
pushManager.fullSyncFlow.test {
|
pushManager.fullSyncFlow.test {
|
||||||
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
|
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Unit,
|
"078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -599,7 +599,7 @@ class PushManagerTest {
|
|||||||
pushManager.fullSyncFlow.test {
|
pushManager.fullSyncFlow.test {
|
||||||
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
|
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Unit,
|
"078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -610,7 +610,7 @@ class PushManagerTest {
|
|||||||
pushManager.fullSyncFlow.test {
|
pushManager.fullSyncFlow.test {
|
||||||
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
|
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Unit,
|
"078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,7 +126,7 @@ class VaultSyncManagerTest {
|
|||||||
private val userLogoutManager: UserLogoutManager = mockk {
|
private val userLogoutManager: UserLogoutManager = mockk {
|
||||||
every { softLogout(any(), any()) } just runs
|
every { softLogout(any(), any()) } just runs
|
||||||
}
|
}
|
||||||
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
private val mutableFullSyncFlow = bufferedMutableSharedFlow<String>()
|
||||||
private val pushManager: PushManager = mockk {
|
private val pushManager: PushManager = mockk {
|
||||||
every { fullSyncFlow } returns mutableFullSyncFlow
|
every { fullSyncFlow } returns mutableFullSyncFlow
|
||||||
}
|
}
|
||||||
@ -1105,15 +1105,27 @@ class VaultSyncManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fullSyncFlow emission should trigger unforced sync`() {
|
fun `fullSyncFlow emission with active user ID should trigger an unforced sync`() {
|
||||||
val userId = "mockId-1"
|
val userId = "mockId-1"
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
every { settingsDiskSource.getLastSyncTime(userId = userId) } returns null
|
|
||||||
coEvery { syncService.sync() } just awaits
|
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")
|
@Suppress("MaxLineLength")
|
||||||
|
|||||||
@ -33,7 +33,9 @@ import org.junit.jupiter.api.Test
|
|||||||
class ImportLoginsViewModelTest : BaseViewModelTest() {
|
class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val vaultRepository: VaultRepository = mockk {
|
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 {
|
private val firstTimeActionManager: FirstTimeActionManager = mockk {
|
||||||
@ -312,21 +314,23 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
coVerify { vaultRepository.syncForResult() }
|
coVerify(exactly = 1) { vaultRepository.syncForResult(forced = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `RetryVaultSync sets isVaultSyncing to true and clears dialog state and calls syncForResult`() =
|
fun `RetryVaultSync sets isVaultSyncing to true and clears dialog state and calls syncForResult`() =
|
||||||
runTest {
|
runTest {
|
||||||
coEvery { vaultRepository.syncForResult() } returns SyncVaultDataResult.Error(Exception())
|
coEvery {
|
||||||
|
vaultRepository.syncForResult(forced = true)
|
||||||
|
} returns SyncVaultDataResult.Error(Exception())
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
assertNotNull(awaitItem().dialogState)
|
assertNotNull(awaitItem().dialogState)
|
||||||
coEvery { vaultRepository.syncForResult() } returns SyncVaultDataResult.Success(
|
coEvery {
|
||||||
itemsAvailable = true,
|
vaultRepository.syncForResult(forced = true)
|
||||||
)
|
} returns SyncVaultDataResult.Success(itemsAvailable = true)
|
||||||
viewModel.trySendAction(ImportLoginsAction.RetryVaultSync)
|
viewModel.trySendAction(ImportLoginsAction.RetryVaultSync)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
@ -339,7 +343,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
coVerify { vaultRepository.syncForResult() }
|
coVerify(exactly = 2) { vaultRepository.syncForResult(forced = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@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`() =
|
fun `MoveToSyncInProgress should set no items imported error dialog state when sync succeeds but no items are available`() =
|
||||||
runTest {
|
runTest {
|
||||||
coEvery {
|
coEvery {
|
||||||
vaultRepository.syncForResult()
|
vaultRepository.syncForResult(forced = true)
|
||||||
} returns SyncVaultDataResult.Success(itemsAvailable = false)
|
} returns SyncVaultDataResult.Success(itemsAvailable = false)
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
@ -402,7 +406,9 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `SyncVaultDataResult Error should remove loading state and show error dialog`() = runTest {
|
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()
|
val viewModel = createViewModel()
|
||||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -420,7 +426,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||||||
fun `FailSyncAcknowledged should remove dialog state and send NavigateBack event`() =
|
fun `FailSyncAcknowledged should remove dialog state and send NavigateBack event`() =
|
||||||
runTest {
|
runTest {
|
||||||
coEvery {
|
coEvery {
|
||||||
vaultRepository.syncForResult()
|
vaultRepository.syncForResult(forced = true)
|
||||||
} returns SyncVaultDataResult.Error(Exception())
|
} returns SyncVaultDataResult.Error(Exception())
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user