PM-1908: Push notifications for non-active accounts prompt for future sync

This commit is contained in:
David Perez 2025-12-09 11:53:52 -06:00
parent 4a874668f2
commit 4641fd8709
No known key found for this signature in database
GPG Key ID: 3E29BD8B1BF090AC
12 changed files with 217 additions and 82 deletions

View File

@ -34,6 +34,8 @@ import java.time.Clock
import java.time.ZoneOffset
import java.time.ZonedDateTime
import javax.inject.Inject
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.toJavaDuration
@ -134,7 +136,6 @@ class PushManagerImpl @Inject constructor(
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun onMessageReceived(notification: BitwardenNotification) {
if (authDiskSource.uniqueAppId == notification.contextId) return
val userId = activeUserId ?: return
Timber.d("Push Notification Received: ${notification.notificationType}")
when (val type = notification.notificationType) {
@ -179,11 +180,13 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncCipherNotification>(
string = notification.payload,
)
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.takeIf { it.cipherId != null && it.revisionDate != null }
.takeIf {
it.cipherId != null && it.revisionDate != null && isLoggedIn(it.userId)
}
?.let {
mutableSyncCipherUpsertSharedFlow.tryEmit(
SyncCipherUpsertData(
userId = requireNotNull(it.userId),
cipherId = requireNotNull(it.cipherId),
revisionDate = requireNotNull(it.revisionDate),
organizationId = it.organizationId,
@ -228,11 +231,13 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncFolderNotification>(
string = notification.payload,
)
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.takeIf { it.folderId != null && it.revisionDate != null }
.takeIf {
it.folderId != null && it.revisionDate != null && isLoggedIn(it.userId)
}
?.let {
mutableSyncFolderUpsertSharedFlow.tryEmit(
SyncFolderUpsertData(
userId = requireNotNull(it.userId),
folderId = requireNotNull(it.folderId),
revisionDate = requireNotNull(it.revisionDate),
isUpdate = type == NotificationType.SYNC_FOLDER_UPDATE,
@ -273,11 +278,13 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncSendNotification>(
string = notification.payload,
)
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.takeIf { it.sendId != null && it.revisionDate != null }
.takeIf {
it.sendId != null && it.revisionDate != null && isLoggedIn(it.userId)
}
?.let {
mutableSyncSendUpsertSharedFlow.tryEmit(
SyncSendUpsertData(
userId = requireNotNull(it.userId),
sendId = requireNotNull(it.sendId),
revisionDate = requireNotNull(it.revisionDate),
isUpdate = type == NotificationType.SYNC_SEND_UPDATE,
@ -361,11 +368,11 @@ class PushManagerImpl @Inject constructor(
)
}
@OptIn(ExperimentalContracts::class)
private fun isLoggedIn(
userId: String,
): Boolean = authDiskSource.getAccountTokens(userId)?.isLoggedIn == true
}
private fun NotificationPayload.userMatchesNotification(userId: String): Boolean {
return this.userId != null && this.userId == userId
userId: String?,
): Boolean {
contract { returns(true) implies (userId != null) }
return userId?.let { authDiskSource.getAccountTokens(it) }?.isLoggedIn == true
}
}

View File

@ -5,12 +5,14 @@ import java.time.ZonedDateTime
/**
* Required data for sync cipher upsert operations.
*
* @property userId The user ID associated with this update.
* @property cipherId The cipher ID.
* @property revisionDate The cipher's revision date. This is used to determine if the local copy of
* the cipher is out-of-date.
* @property isUpdate Whether or not this is an update of an existing cipher.
*/
data class SyncCipherUpsertData(
val userId: String,
val cipherId: String,
val revisionDate: ZonedDateTime,
val organizationId: String?,

View File

@ -5,12 +5,14 @@ import java.time.ZonedDateTime
/**
* Required data for sync folder upsert operations.
*
* @property userId The user ID associated with this update.
* @property folderId The folder ID.
* @property revisionDate The folder's revision date. This is used to determine if the local copy of
* the folder is out-of-date.
* @property isUpdate Whether or not this is an update of an existing folder.
*/
data class SyncFolderUpsertData(
val userId: String,
val folderId: String,
val revisionDate: ZonedDateTime,
val isUpdate: Boolean,

View File

@ -5,12 +5,14 @@ import java.time.ZonedDateTime
/**
* Required data for sync send upsert operations.
*
* @property userId The user ID associated with this update.
* @property sendId The send ID.
* @property revisionDate The send's revision date. This is used to determine if the local copy of
* the send is out-of-date.
* @property isUpdate Whether or not this is an update of an existing send.
*/
data class SyncSendUpsertData(
val userId: String,
val sendId: String,
val revisionDate: ZonedDateTime,
val isUpdate: Boolean,

View File

@ -17,6 +17,7 @@ import com.bitwarden.vault.AttachmentView
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.EncryptionContext
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
@ -53,6 +54,7 @@ import java.time.Clock
class CipherManagerImpl(
private val fileManager: FileManager,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val ciphersService: CiphersService,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
@ -689,7 +691,7 @@ class CipherManagerImpl(
* for now.
*/
private suspend fun syncCipherIfNecessary(syncCipherUpsertData: SyncCipherUpsertData) {
val userId = activeUserId ?: return
val userId = syncCipherUpsertData.userId
val cipherId = syncCipherUpsertData.cipherId
val organizationId = syncCipherUpsertData.organizationId
val collectionIds = syncCipherUpsertData.collectionIds
@ -732,6 +734,12 @@ class CipherManagerImpl(
}
if (!shouldUpdate) return
if (activeUserId != userId) {
// We cannot update right now since the accounts do not match, so we will
// do a full-sync on the next check.
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
return
}
ciphersService
.getCipher(cipherId = cipherId)

View File

@ -6,6 +6,7 @@ import com.bitwarden.network.model.UpdateFolderResponseJson
import com.bitwarden.network.service.FolderService
import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
@ -25,8 +26,10 @@ import kotlinx.coroutines.flow.onEach
/**
* The default implementation of the [FolderManager].
*/
@Suppress("LongParameterList")
class FolderManagerImpl(
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val folderService: FolderService,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
@ -148,7 +151,7 @@ class FolderManagerImpl(
* are met.
*/
private suspend fun syncFolderIfNecessary(syncFolderUpsertData: SyncFolderUpsertData) {
val userId = activeUserId ?: return
val userId = syncFolderUpsertData.userId
val folderId = syncFolderUpsertData.folderId
val isUpdate = syncFolderUpsertData.isUpdate
val revisionDate = syncFolderUpsertData.revisionDate
@ -162,6 +165,12 @@ class FolderManagerImpl(
localFolder.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
if (!isValidCreate && !isValidUpdate) return
if (activeUserId != userId) {
// We cannot update right now since the accounts do not match, so we will
// do a full-sync on the next check.
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
return
}
folderService
.getFolder(folderId = folderId)

View File

@ -13,6 +13,7 @@ import com.bitwarden.send.Send
import com.bitwarden.send.SendType
import com.bitwarden.send.SendView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
@ -38,6 +39,7 @@ import retrofit2.HttpException
@Suppress("LongParameterList")
class SendManagerImpl(
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val sendsService: SendsService,
@ -265,7 +267,7 @@ class SendManagerImpl(
* now.
*/
private suspend fun syncSendIfNecessary(syncSendUpsertData: SyncSendUpsertData) {
val userId = activeUserId ?: return
val userId = syncSendUpsertData.userId
val sendId = syncSendUpsertData.sendId
val isUpdate = syncSendUpsertData.isUpdate
val revisionDate = syncSendUpsertData.revisionDate
@ -278,6 +280,12 @@ class SendManagerImpl(
localSend != null &&
localSend.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
if (!isValidCreate && !isValidUpdate) return
if (activeUserId != userId) {
// We cannot update right now since the accounts do not match, so we will
// do a full-sync on the next check.
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
return
}
sendsService
.getSend(sendId = sendId)

View File

@ -61,6 +61,7 @@ object VaultManagerModule {
@Singleton
fun provideCipherManager(
ciphersService: CiphersService,
settingsDiskSource: SettingsDiskSource,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
@ -71,6 +72,7 @@ object VaultManagerModule {
pushManager: PushManager,
): CipherManager = CipherManagerImpl(
fileManager = fileManager,
settingsDiskSource = settingsDiskSource,
authDiskSource = authDiskSource,
ciphersService = ciphersService,
vaultDiskSource = vaultDiskSource,
@ -85,6 +87,7 @@ object VaultManagerModule {
@Singleton
fun provideFolderManager(
folderService: FolderService,
settingsDiskSource: SettingsDiskSource,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
@ -92,6 +95,7 @@ object VaultManagerModule {
pushManager: PushManager,
): FolderManager = FolderManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
folderService = folderService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
@ -106,6 +110,7 @@ object VaultManagerModule {
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
fileManager: FileManager,
reviewPromptManager: ReviewPromptManager,
pushManager: PushManager,
@ -113,6 +118,7 @@ object VaultManagerModule {
): SendManager = SendManagerImpl(
fileManager = fileManager,
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
sendsService = sendsService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,

View File

@ -260,6 +260,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_CIPHER_CREATE_NOTIFICATION_MAP)
assertEquals(
SyncCipherUpsertData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
organizationId = "6a41d965-ed95-4eae-98c3-5f1ec609c2c1",
collectionIds = listOf(),
@ -293,6 +294,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_CIPHER_UPDATE_NOTIFICATION_MAP)
assertEquals(
SyncCipherUpsertData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
organizationId = "6a41d965-ed95-4eae-98c3-5f1ec609c2c1",
collectionIds = listOf(),
@ -311,6 +313,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_FOLDER_CREATE_NOTIFICATION_MAP)
assertEquals(
SyncFolderUpsertData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"),
isUpdate = false,
@ -342,6 +345,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_FOLDER_UPDATE_NOTIFICATION_MAP)
assertEquals(
SyncFolderUpsertData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"),
isUpdate = true,
@ -372,6 +376,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_SEND_CREATE_NOTIFICATION_MAP)
assertEquals(
SyncSendUpsertData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"),
isUpdate = false,
@ -401,6 +406,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_SEND_UPDATE_NOTIFICATION_MAP)
assertEquals(
SyncSendUpsertData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"),
isUpdate = true,
@ -437,7 +443,8 @@ class PushManagerTest {
}
@Test
fun `onMessageReceived with sync cipher create does nothing`() = runTest {
fun `onMessageReceived with sync cipher create emits to syncCipherUpsertFlow`() =
runTest {
pushManager.syncCipherUpsertFlow.test {
pushManager.onMessageReceived(SYNC_CIPHER_CREATE_NOTIFICATION_MAP)
expectNoEvents()
@ -459,7 +466,8 @@ class PushManagerTest {
}
@Test
fun `onMessageReceived with sync cipher update does nothing`() = runTest {
fun `onMessageReceived with sync cipher update emits to syncCipherUpsertFlow`() =
runTest {
pushManager.syncCipherUpsertFlow.test {
pushManager.onMessageReceived(SYNC_CIPHER_UPDATE_NOTIFICATION_MAP)
expectNoEvents()
@ -543,62 +551,6 @@ class PushManagerTest {
}
}
@Nested
inner class NullUserState {
@BeforeEach
fun setUp() {
authDiskSource.userState = null
}
@Test
fun `onMessageReceived with logout does nothing`() = runTest {
pushManager.logoutFlow.test {
pushManager.onMessageReceived(LOGOUT_NOTIFICATION_MAP)
expectNoEvents()
}
}
@Test
fun `onMessageReceived with logout with kdf reason does nothing`() = runTest {
pushManager.logoutFlow.test {
pushManager.onMessageReceived(LOGOUT_KDF_NOTIFICATION_MAP)
expectNoEvents()
}
}
@Test
fun `onMessageReceived with sync ciphers does nothing`() = runTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_MAP)
expectNoEvents()
}
}
@Test
fun `onMessageReceived with sync org keys does nothing`() = runTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_ORG_KEYS_NOTIFICATION_MAP)
expectNoEvents()
}
}
@Test
fun `onMessageReceived with sync settings does nothing`() = runTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_MAP)
expectNoEvents()
}
}
@Test
fun `onMessageReceived with sync vault does nothing`() = runTest {
pushManager.fullSyncFlow.test {
pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_MAP)
expectNoEvents()
}
}
}
@Nested
inner class NonNullUserState {
@BeforeEach

View File

@ -29,6 +29,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
@ -91,6 +92,7 @@ class CipherManagerTest {
coEvery { delete(*anyVararg()) } just runs
}
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val ciphersService: CiphersService = mockk()
private val vaultDiskSource: VaultDiskSource = mockk()
private val vaultSdkSource: VaultSdkSource = mockk()
@ -106,6 +108,7 @@ class CipherManagerTest {
private val cipherManager: CipherManager = CipherManagerImpl(
ciphersService = ciphersService,
settingsDiskSource = fakeSettingsDiskSource,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = fakeAuthDiskSource,
@ -2403,6 +2406,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = false,
@ -2450,6 +2454,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = false,
@ -2492,6 +2497,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = true,
@ -2518,6 +2524,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = true,
@ -2552,6 +2559,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
isUpdate = true,
@ -2589,6 +2597,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = true,
@ -2620,6 +2629,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = false,
@ -2659,6 +2669,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = false,
@ -2697,6 +2708,7 @@ class CipherManagerTest {
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = true,
@ -2712,6 +2724,43 @@ class CipherManagerTest {
}
}
@Test
fun `syncCipherUpsertFlow with inactive userId should clear the last sync time`() = runTest {
val number = 1
val userId = "nonActiveUserId"
val cipherId = "mockId-$number"
val originalCipher = mockk<SyncResponseJson.Cipher> {
every { revisionDate } returns ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES)
}
val lastSyncTime = clock.instant()
fakeSettingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = lastSyncTime)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
} returns originalCipher
mutableSyncCipherUpsertFlow.tryEmit(
SyncCipherUpsertData(
userId = userId,
cipherId = cipherId,
revisionDate = ZonedDateTime.now(clock),
isUpdate = true,
collectionIds = null,
organizationId = null,
),
)
fakeSettingsDiskSource.assertLastSyncTime(userId = userId, expected = null)
coVerify(exactly = 1) {
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
}
coVerify(exactly = 0) {
ciphersService.getCipher(cipherId)
vaultDiskSource.saveCipher(userId = userId, cipher = any())
}
}
private fun setupMockUri(
url: String,
queryParams: Map<String, String> = emptyMap(),

View File

@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
@ -49,6 +50,7 @@ import java.time.temporal.ChronoUnit
class FolderManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val folderService = mockk<FolderService>()
private val vaultDiskSource = mockk<VaultDiskSource>()
private val vaultSdkSource = mockk<VaultSdkSource>()
@ -61,6 +63,7 @@ class FolderManagerTest {
private val folderManager: FolderManager = FolderManagerImpl(
authDiskSource = fakeAuthDiskSource,
settingsDiskSource = fakeSettingsDiskSource,
folderService = folderService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
@ -435,6 +438,7 @@ class FolderManagerTest {
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
userId = userId,
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
@ -460,6 +464,7 @@ class FolderManagerTest {
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
userId = userId,
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
@ -486,6 +491,7 @@ class FolderManagerTest {
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
userId = userId,
folderId = folderId,
revisionDate = ZonedDateTime.ofInstant(
Instant.ofEpochSecond(0), ZoneId.of("UTC"),
@ -518,6 +524,7 @@ class FolderManagerTest {
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
userId = userId,
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
@ -552,6 +559,7 @@ class FolderManagerTest {
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
userId = userId,
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
@ -563,6 +571,42 @@ class FolderManagerTest {
vaultDiskSource.saveFolder(userId = userId, folder = folder)
}
}
@Test
fun `syncFolderUpsertFlow with inactive userId should clear the last sync time`() = runTest {
val number = 1
val userId = "nonActiveUserId"
val folderId = "mockId-$number"
val lastSyncTime = FIXED_CLOCK.instant()
fakeSettingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = lastSyncTime)
fakeAuthDiskSource.userState = MOCK_USER_STATE
val folderView = createMockFolder(
number = number,
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
)
coEvery {
vaultDiskSource.getFolders(userId = userId)
} returns MutableStateFlow(listOf(folderView))
mutableSyncFolderUpsertFlow.tryEmit(
SyncFolderUpsertData(
userId = userId,
folderId = folderId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
),
)
fakeSettingsDiskSource.assertLastSyncTime(userId = userId, expected = null)
coVerify(exactly = 1) {
vaultDiskSource.getFolders(userId = userId)
}
coVerify(exactly = 0) {
folderService.getFolder(folderId = folderId)
vaultDiskSource.saveFolder(userId = userId, folder = any())
}
}
}
private val FIXED_CLOCK: Clock = Clock.fixed(

View File

@ -20,6 +20,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
@ -64,6 +65,7 @@ class SendManagerTest {
coEvery { delete(files = anyVararg()) } just runs
}
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val sendsService = mockk<SendsService>()
private val vaultDiskSource = mockk<VaultDiskSource>()
private val vaultSdkSource = mockk<VaultSdkSource>()
@ -82,6 +84,7 @@ class SendManagerTest {
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = fakeAuthDiskSource,
settingsDiskSource = fakeSettingsDiskSource,
fileManager = fileManager,
reviewPromptManager = reviewPromptManager,
pushManager = pushManager,
@ -129,6 +132,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
@ -152,6 +156,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
@ -182,6 +187,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
isUpdate = true,
@ -221,6 +227,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
@ -251,6 +258,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
@ -283,6 +291,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = false,
@ -318,6 +327,7 @@ class SendManagerTest {
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
@ -330,6 +340,42 @@ class SendManagerTest {
}
}
@Test
fun `syncSendUpsertFlow with inactive userId should clear the last sync time`() = runTest {
val number = 1
val userId = "nonActiveUserId"
val sendId = "mockId-$number"
val lastSyncTime = FIXED_CLOCK.instant()
fakeSettingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = lastSyncTime)
fakeAuthDiskSource.userState = MOCK_USER_STATE
val sendView = createMockSend(
number = number,
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
)
coEvery {
vaultDiskSource.getSends(userId = userId)
} returns MutableStateFlow(listOf(sendView))
mutableSyncSendUpsertFlow.tryEmit(
SyncSendUpsertData(
userId = userId,
sendId = sendId,
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
isUpdate = true,
),
)
fakeSettingsDiskSource.assertLastSyncTime(userId = userId, expected = null)
coVerify(exactly = 1) {
vaultDiskSource.getSends(userId = userId)
}
coVerify(exactly = 0) {
sendsService.getSend(sendId = sendId)
vaultDiskSource.saveSend(userId = userId, send = any())
}
}
@Test
fun `createSend with no active user should return CreateSendResult Error`() =
runTest {