PM-25462: Allow SYNC_FOLDER_DELETE notification to delete Folders for inactive user (#5832)

This commit is contained in:
David Perez 2025-09-04 12:31:00 -05:00 committed by GitHub
parent eec4233486
commit aa39e6c6be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 46 additions and 58 deletions

View File

@ -227,9 +227,14 @@ class PushManagerImpl @Inject constructor(
.decodeFromString<NotificationPayload.SyncFolderNotification>(
string = notification.payload,
)
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.folderId
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(SyncFolderDeleteData(it)) }
.takeIf { it.userId != null && it.folderId != null }
?.let {
SyncFolderDeleteData(
userId = requireNotNull(it.userId),
folderId = requireNotNull(it.folderId),
)
}
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(it) }
}
NotificationType.SYNC_ORG_KEYS -> {

View File

@ -2,9 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
/**
* Required data for sync folder delete operations.
*
* @property folderId The folder ID.
*/
data class SyncFolderDeleteData(
val userId: String,
val folderId: String,
)

View File

@ -929,11 +929,9 @@ class VaultRepositoryImpl(
}
private suspend fun clearFolderIdFromCiphers(folderId: String, userId: String) {
vaultDiskSource.getCiphersFlow(userId).firstOrNull()?.forEach {
vaultDiskSource.getCiphers(userId = userId).forEach {
if (it.folderId == folderId) {
vaultDiskSource.saveCipher(
userId, it.copy(folderId = null),
)
vaultDiskSource.saveCipher(userId = userId, cipher = it.copy(folderId = null))
}
}
}
@ -1433,16 +1431,13 @@ class VaultRepositoryImpl(
* Deletes the folder specified by [syncFolderDeleteData] from disk.
*/
private suspend fun deleteFolder(syncFolderDeleteData: SyncFolderDeleteData) {
val userId = activeUserId ?: return
val folderId = syncFolderDeleteData.folderId
clearFolderIdFromCiphers(
folderId = folderId,
userId = userId,
folderId = syncFolderDeleteData.folderId,
userId = syncFolderDeleteData.userId,
)
vaultDiskSource.deleteFolder(
folderId = folderId,
userId = userId,
folderId = syncFolderDeleteData.folderId,
userId = syncFolderDeleteData.userId,
)
}

View File

@ -285,6 +285,7 @@ class PushManagerTest {
pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_MAP)
assertEquals(
SyncFolderDeleteData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
),
awaitItem(),
@ -425,12 +426,19 @@ class PushManagerTest {
}
@Test
fun `onMessageReceived with sync folder delete does nothing`() = runTest {
pushManager.syncFolderDeleteFlow.test {
pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_MAP)
expectNoEvents()
fun `onMessageReceived with sync folder delete emits to syncFolderDeleteFlow`() =
runTest {
pushManager.syncFolderDeleteFlow.test {
pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_MAP)
assertEquals(
SyncFolderDeleteData(
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
),
awaitItem(),
)
}
}
}
@Test
fun `onMessageReceived with sync folder update does nothing`() = runTest {

View File

@ -2834,46 +2834,26 @@ class VaultRepositoryTest {
fun `DeleteFolder with folderService Delete success should return DeleteFolderResult Success and update ciphers`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = MOCK_USER_STATE.activeUserId
val folderId = "mockFolderId-1"
val mockCipher = createMockCipher(number = 1)
val ciphers = listOf(mockCipher, createMockCipher(number = 2))
coEvery { folderService.deleteFolder(folderId) } returns Unit.asSuccess()
coEvery {
vaultDiskSource.deleteFolder(
MOCK_USER_STATE.activeUserId,
folderId,
)
} just runs
val mockCipher = createMockCipher(1)
val mutableCiphersStateFlow =
MutableStateFlow(
listOf(
mockCipher,
createMockCipher(2),
),
)
coEvery {
vaultDiskSource.getCiphersFlow(MOCK_USER_STATE.activeUserId)
} returns mutableCiphersStateFlow
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns ciphers
coEvery {
vaultDiskSource.saveCipher(
MOCK_USER_STATE.activeUserId,
mockCipher.copy(
folderId = null,
),
userId = userId,
cipher = mockCipher.copy(folderId = null),
)
} just runs
val result = vaultRepository.deleteFolder(folderId)
val result = vaultRepository.deleteFolder(folderId = folderId)
coVerify(exactly = 1) {
vaultDiskSource.saveCipher(
MOCK_USER_STATE.activeUserId,
mockCipher.copy(
folderId = null,
),
userId = userId,
cipher = mockCipher.copy(folderId = null),
)
}
@ -4020,20 +4000,21 @@ class VaultRepositoryTest {
fun `syncFolderDeleteFlow should delete folder from disk and update ciphers`() {
val userId = "mockId-1"
val folderId = "mockId-1"
val cipher = createMockCipher(number = 1, folderId = folderId)
val updatedCipher = createMockCipher(number = 1, folderId = null)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
coEvery {
vaultDiskSource.getCiphersFlow(userId)
} returns flowOf()
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns listOf(cipher)
coEvery { vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher) } just runs
mutableSyncFolderDeleteFlow.tryEmit(
SyncFolderDeleteData(folderId = folderId),
SyncFolderDeleteData(userId = userId, folderId = folderId),
)
coVerify {
coVerify(exactly = 1) {
vaultDiskSource.deleteFolder(userId = userId, folderId = folderId)
vaultDiskSource.getCiphersFlow(userId)
vaultDiskSource.getCiphers(userId = userId)
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
}
}