From 2bd4834b14d1e199aaa8a9ee4f0db812c27dba53 Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 5 Sep 2025 09:32:49 -0500 Subject: [PATCH] PM-25478: Update sends and folders while vault is locked (#5837) --- .../vault/repository/VaultRepositoryImpl.kt | 19 +- .../vault/repository/VaultRepositoryTest.kt | 315 ++++++------------ 2 files changed, 107 insertions(+), 227 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 76dd3d60c7..3a1e429b6e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -114,7 +114,6 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -1397,15 +1396,14 @@ class VaultRepositoryImpl( val isUpdate = syncSendUpsertData.isUpdate val revisionDate = syncSendUpsertData.revisionDate - val localSend = sendDataStateFlow - .mapNotNull { it.data } + val localSend = vaultDiskSource + .getSends(userId = userId) .first() - .sendViewList .find { it.id == sendId } val isValidCreate = !isUpdate && localSend == null val isValidUpdate = isUpdate && localSend != null && - localSend.revisionDate.epochSecond < revisionDate.toEpochSecond() + localSend.revisionDate.toEpochSecond() < revisionDate.toEpochSecond() if (!isValidCreate && !isValidUpdate) return @@ -1447,21 +1445,20 @@ class VaultRepositoryImpl( val folderId = syncFolderUpsertData.folderId val isUpdate = syncFolderUpsertData.isUpdate val revisionDate = syncFolderUpsertData.revisionDate - - val localFolder = foldersStateFlow - .mapNotNull { it.data } + val localFolder = vaultDiskSource + .getFolders(userId = userId) .first() .find { it.id == folderId } val isValidCreate = !isUpdate && localFolder == null val isValidUpdate = isUpdate && localFolder != null && - localFolder.revisionDate.epochSecond < revisionDate.toEpochSecond() + localFolder.revisionDate.toEpochSecond() < revisionDate.toEpochSecond() if (!isValidCreate && !isValidUpdate) return folderService - .getFolder(folderId) - .onSuccess { vaultDiskSource.saveFolder(userId, it) } + .getFolder(folderId = folderId) + .onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) } } //endregion Push Notification helpers diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 2fc25ae20a..c4fa67b049 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -3788,53 +3788,37 @@ class VaultRepositoryTest { fun `syncSendUpsertFlow update failure with 404 code should make a request for a send and then delete it`() = runTest { val number = 1 + val userId = MOCK_USER_STATE.activeUserId val sendId = "mockId-$number" + fakeAuthDiskSource.userState = MOCK_USER_STATE val response: HttpException = mockk { every { code() } returns 404 } + coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure() coEvery { - sendsService.getSend(sendId) - } returns response.asFailure() - - coEvery { - vaultDiskSource.deleteSend(any(), any()) + vaultDiskSource.deleteSend(userId = userId, sendId = sendId) } just runs - fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId) - val sendView = createMockSendView(number = number) + val sendView = createMockSend( + number = number, + revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES), + ) coEvery { - vaultSdkSource.decryptSendList( - userId = MOCK_USER_STATE.activeUserId, - sendList = listOf(createMockSdkSend(number = number)), - ) - } returns listOf(sendView).asSuccess() + vaultDiskSource.getSends(userId = userId) + } returns MutableStateFlow(listOf(sendView)) - val sendsFlow = bufferedMutableSharedFlow>() - setupVaultDiskSourceFlows(sendsFlow = sendsFlow) - - vaultRepository.sendDataStateFlow.test { - // Populate and consume items related to the sends flow - awaitItem() - sendsFlow.tryEmit(listOf(createMockSend(number = number))) - awaitItem() - - mutableSyncSendUpsertFlow.tryEmit( - SyncSendUpsertData( - sendId = sendId, - revisionDate = ZonedDateTime.now(), - isUpdate = true, - ), - ) - } + mutableSyncSendUpsertFlow.tryEmit( + SyncSendUpsertData( + sendId = sendId, + revisionDate = ZonedDateTime.now(clock), + isUpdate = true, + ), + ) coVerify(exactly = 1) { - sendsService.getSend(sendId) - vaultDiskSource.deleteSend( - userId = MOCK_USER_STATE.activeUserId, - sendId = sendId, - ) + sendsService.getSend(sendId = sendId) + vaultDiskSource.deleteSend(userId = userId, sendId = sendId) } } @@ -3842,51 +3826,31 @@ class VaultRepositoryTest { @Test fun `syncSendUpsertFlow create failure with 404 code should make a request for a send and do nothing`() = runTest { + val userId = MOCK_USER_STATE.activeUserId val sendId = "mockId-1" fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId) - val response: HttpException = mockk { every { code() } returns 404 } + coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure() coEvery { - sendsService.getSend(sendId) - } returns response.asFailure() + vaultDiskSource.getSends(userId = userId) + } returns MutableStateFlow(emptyList()) - coEvery { - vaultSdkSource.decryptSendList( - userId = MOCK_USER_STATE.activeUserId, - sendList = listOf(), - ) - } returns emptyList().asSuccess() - - val sendsFlow = bufferedMutableSharedFlow>() - setupVaultDiskSourceFlows(sendsFlow = sendsFlow) - - vaultRepository.sendDataStateFlow.test { - // Populate and consume items related to the sends flow - awaitItem() - sendsFlow.tryEmit(emptyList()) - awaitItem() - - mutableSyncSendUpsertFlow.tryEmit( - SyncSendUpsertData( - sendId = sendId, - revisionDate = ZonedDateTime.now(), - isUpdate = false, - ), - ) - } + mutableSyncSendUpsertFlow.tryEmit( + SyncSendUpsertData( + sendId = sendId, + revisionDate = ZonedDateTime.now(clock), + isUpdate = false, + ), + ) coVerify(exactly = 1) { - sendsService.getSend(sendId) + sendsService.getSend(sendId = sendId) } coVerify(exactly = 0) { - vaultDiskSource.deleteSend( - userId = MOCK_USER_STATE.activeUserId, - sendId = sendId, - ) + vaultDiskSource.deleteSend(userId = userId, sendId = sendId) } } @@ -3895,50 +3859,28 @@ class VaultRepositoryTest { fun `syncSendUpsertFlow valid create success should make a request for a send and then store it`() = runTest { val number = 1 + val userId = MOCK_USER_STATE.activeUserId val sendId = "mockId-$number" fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId) coEvery { - vaultSdkSource.decryptSendList( - userId = MOCK_USER_STATE.activeUserId, - sendList = listOf(), - ) - } returns listOf().asSuccess() + vaultDiskSource.getSends(userId = userId) + } returns MutableStateFlow(emptyList()) + val send = mockk() + coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess() + coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs - val sendsFlow = bufferedMutableSharedFlow>() - setupVaultDiskSourceFlows(sendsFlow = sendsFlow) - - val send: SyncResponseJson.Send = mockk() - coEvery { - sendsService.getSend(sendId) - } returns send.asSuccess() - - coEvery { - vaultDiskSource.saveSend(any(), any()) - } just runs - - vaultRepository.sendDataStateFlow.test { - // Populate and consume items related to the sends flow - awaitItem() - sendsFlow.tryEmit(listOf()) - awaitItem() - - mutableSyncSendUpsertFlow.tryEmit( - SyncSendUpsertData( - sendId = sendId, - revisionDate = ZonedDateTime.now(), - isUpdate = false, - ), - ) - } + mutableSyncSendUpsertFlow.tryEmit( + SyncSendUpsertData( + sendId = sendId, + revisionDate = ZonedDateTime.now(clock), + isUpdate = false, + ), + ) coVerify(exactly = 1) { - sendsService.getSend(sendId) - vaultDiskSource.saveSend( - userId = MOCK_USER_STATE.activeUserId, - send = send, - ) + sendsService.getSend(sendId = sendId) + vaultDiskSource.saveSend(userId = userId, send = send) } } @@ -3947,51 +3889,33 @@ class VaultRepositoryTest { fun `syncSendUpsertFlow valid update success should make a request for a send and then store it`() = runTest { val number = 1 + val userId = MOCK_USER_STATE.activeUserId val sendId = "mockId-$number" fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId) - val sendView = createMockSendView(number = number) + val sendView = createMockSend( + number = number, + revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES), + ) coEvery { - vaultSdkSource.decryptSendList( - userId = MOCK_USER_STATE.activeUserId, - sendList = listOf(createMockSdkSend(number = number)), - ) - } returns listOf(sendView).asSuccess() + vaultDiskSource.getSends(userId = userId) + } returns MutableStateFlow(listOf(sendView)) - val sendsFlow = bufferedMutableSharedFlow>() - setupVaultDiskSourceFlows(sendsFlow = sendsFlow) + val send = mockk() + coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess() + coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs - val send: SyncResponseJson.Send = mockk() - coEvery { - sendsService.getSend(sendId) - } returns send.asSuccess() - - coEvery { - vaultDiskSource.saveSend(any(), any()) - } just runs - - vaultRepository.sendDataStateFlow.test { - // Populate and consume items related to the sends flow - awaitItem() - sendsFlow.tryEmit(listOf(createMockSend(number = number))) - awaitItem() - - mutableSyncSendUpsertFlow.tryEmit( - SyncSendUpsertData( - sendId = sendId, - revisionDate = ZonedDateTime.now(), - isUpdate = true, - ), - ) - } + mutableSyncSendUpsertFlow.tryEmit( + SyncSendUpsertData( + sendId = sendId, + revisionDate = ZonedDateTime.now(clock), + isUpdate = true, + ), + ) coVerify(exactly = 1) { - sendsService.getSend(sendId) - vaultDiskSource.saveSend( - userId = MOCK_USER_STATE.activeUserId, - send = send, - ) + sendsService.getSend(sendId = sendId) + vaultDiskSource.saveSend(userId = userId, send = send) } } @@ -4140,50 +4064,28 @@ class VaultRepositoryTest { fun `syncFolderUpsertFlow valid create success should make a request for a folder and then store it`() = runTest { val number = 1 + val userId = MOCK_USER_STATE.activeUserId val folderId = "mockId-$number" fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId) coEvery { - vaultSdkSource.decryptFolderList( - userId = MOCK_USER_STATE.activeUserId, - folderList = listOf(), - ) - } returns listOf().asSuccess() + vaultDiskSource.getFolders(userId = userId) + } returns MutableStateFlow(emptyList()) + val folder = mockk() + coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess() + coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs - val foldersFlow = bufferedMutableSharedFlow>() - setupVaultDiskSourceFlows(foldersFlow = foldersFlow) - - val folder: SyncResponseJson.Folder = mockk() - coEvery { - folderService.getFolder(folderId) - } returns folder.asSuccess() - - coEvery { - vaultDiskSource.saveFolder(any(), any()) - } just runs - - vaultRepository.foldersStateFlow.test { - // Populate and consume items related to the folders flow - awaitItem() - foldersFlow.tryEmit(listOf()) - awaitItem() - - mutableSyncFolderUpsertFlow.tryEmit( - SyncFolderUpsertData( - folderId = folderId, - revisionDate = ZonedDateTime.now(), - isUpdate = false, - ), - ) - } + mutableSyncFolderUpsertFlow.tryEmit( + SyncFolderUpsertData( + folderId = folderId, + revisionDate = ZonedDateTime.now(clock), + isUpdate = false, + ), + ) coVerify(exactly = 1) { - folderService.getFolder(folderId) - vaultDiskSource.saveFolder( - userId = MOCK_USER_STATE.activeUserId, - folder = folder, - ) + folderService.getFolder(folderId = folderId) + vaultDiskSource.saveFolder(userId = userId, folder = folder) } } @@ -4192,51 +4094,32 @@ class VaultRepositoryTest { fun `syncFolderUpsertFlow valid update success should make a request for a folder and then store it`() = runTest { val number = 1 + val userId = MOCK_USER_STATE.activeUserId val folderId = "mockId-$number" fakeAuthDiskSource.userState = MOCK_USER_STATE - setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId) - val folderView = createMockFolderView(number = number) + val folderView = createMockFolder( + number = number, + revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES), + ) coEvery { - vaultSdkSource.decryptFolderList( - userId = MOCK_USER_STATE.activeUserId, - folderList = listOf(createMockSdkFolder(number = number)), - ) - } returns listOf(folderView).asSuccess() + vaultDiskSource.getFolders(userId = userId) + } returns MutableStateFlow(listOf(folderView)) + val folder = mockk() + coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess() + coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs - val foldersFlow = bufferedMutableSharedFlow>() - setupVaultDiskSourceFlows(foldersFlow = foldersFlow) - - val folder: SyncResponseJson.Folder = mockk() - coEvery { - folderService.getFolder(folderId) - } returns folder.asSuccess() - - coEvery { - vaultDiskSource.saveFolder(any(), any()) - } just runs - - vaultRepository.foldersStateFlow.test { - // Populate and consume items related to the folders flow - awaitItem() - foldersFlow.tryEmit(listOf(createMockFolder(number = number))) - awaitItem() - - mutableSyncFolderUpsertFlow.tryEmit( - SyncFolderUpsertData( - folderId = folderId, - revisionDate = ZonedDateTime.now(), - isUpdate = true, - ), - ) - } + mutableSyncFolderUpsertFlow.tryEmit( + SyncFolderUpsertData( + folderId = folderId, + revisionDate = ZonedDateTime.now(clock), + isUpdate = true, + ), + ) coVerify(exactly = 1) { folderService.getFolder(folderId) - vaultDiskSource.saveFolder( - userId = MOCK_USER_STATE.activeUserId, - folder = folder, - ) + vaultDiskSource.saveFolder(userId = userId, folder = folder) } }